Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9f51ebc
feat(udf-sdk): add comet-udf-sdk crate skeleton
andygrove May 10, 2026
05770a5
chore(udf-sdk): inherit workspace metadata and fix doc link
andygrove May 10, 2026
c4a59fe
feat(udf-sdk): add type tags and field IPC helpers
andygrove May 10, 2026
97b22e6
chore(udf-sdk): re-export IPC helpers and tighten field round-trip test
andygrove May 10, 2026
e0d7669
feat(udf-sdk): add CometUdfError and #[repr(C)] UdfError
andygrove May 10, 2026
275fc86
chore(udf-sdk): tighten UdfError ergonomics and pin layout
andygrove May 10, 2026
fea30b5
feat(udf-sdk): add CometScalarUdf trait and UdfDescriptor
andygrove May 10, 2026
b345d61
chore(udf-sdk): pin UdfDescriptor layout and tidy imports
andygrove May 10, 2026
54652a2
feat(udf-sdk): add export! macro and FFI invoke implementation
andygrove May 10, 2026
4bfbc2d
chore(udf-sdk): add null guards and tighten export! macro docs
andygrove May 10, 2026
51cf22d
feat(udf-sdk): add optional DataFusion ScalarUDFImpl adapter
andygrove May 10, 2026
d058ee8
chore(udf-sdk): make adapter constructor fallible and cache config op…
andygrove May 10, 2026
88a6925
test(udf-sdk): add comet-test-udfs cdylib with AddOne
andygrove May 10, 2026
fa3ebf8
docs(test-udfs): clarify intentional duplication with SDK fixture
andygrove May 10, 2026
e824d82
test(udf-sdk): add struct/error/panic/length-mismatch test UDFs
andygrove May 10, 2026
b4bec8c
feat(rust-udf): add libloading dep and host module skeleton
andygrove May 10, 2026
174003c
feat(rust-udf): add cdylib loader with ABI/symbol checks
andygrove May 10, 2026
5adb3e9
chore(rust-udf): fix sdk dep placement and add SAFETY comments to loader
andygrove May 10, 2026
c8f3f66
feat(rust-udf): add process-wide library cache
andygrove May 10, 2026
797b026
feat(rust-udf): add RustUdfAdapter impl ScalarUDFImpl
andygrove May 10, 2026
acd738d
chore(rust-udf): cache invoke fn pointers and check return type
andygrove May 10, 2026
62a948d
feat(rust-udf): add RustUdfCall protobuf message
andygrove May 10, 2026
894c507
feat(rust-udf): handle RustUdfCall in PhysicalPlanner
andygrove May 10, 2026
2da5056
feat(rust-udf): add JNI bridge for driver-side library validation
andygrove May 10, 2026
1900f48
feat(rust-udf): add typed exception classes
andygrove May 10, 2026
a8f8eca
feat(rust-udf): add CometRustUdfRegistry
andygrove May 10, 2026
4f18e39
feat(rust-udf): add CometRustUDF.register / registerAll
andygrove May 10, 2026
6a548ba
feat(rust-udf): emit RustUdfCall proto for registered names
andygrove May 10, 2026
6fbde43
test(rust-udf): add end-to-end CometRustUdfSuite
andygrove May 10, 2026
f6d420b
docs(rust-udf): add user guide for custom Rust UDFs
andygrove May 10, 2026
548b7a5
chore(rust-udf): apply review feedback
andygrove May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ under the License.
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalatestplus</groupId>
<artifactId>junit-4-13_${scala.binary.version}</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -175,6 +185,10 @@ under the License.
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
Expand All @@ -192,6 +206,18 @@ under the License.
</sources>
</configuration>
</execution>
<execution>
<id>add-scala-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/scala</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Expand Down
42 changes: 42 additions & 0 deletions common/src/main/java/org/apache/comet/udf/CometRustUdfBridge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.comet.udf;

import org.apache.comet.NativeBase;

/** JNI bridge for driver-side Rust UDF library validation. */
public final class CometRustUdfBridge extends NativeBase {
private CometRustUdfBridge() {}

/**
* Validate that {@code libraryPath} loads, exposes a UDF named {@code expectedName}, and return a
* JSON description of that UDF. Throws RuntimeException on any error.
*
* <p>The returned JSON has the form: {@code
* {"name":"add_one","args":["Int64"],"return_type":"Int64","volatility":0}}
*/
public static native String validateLibrary(String libraryPath, String expectedName);

/**
* Return a JSON array describing every UDF exposed by {@code libraryPath}. Each element has the
* same shape as the return value of {@link #validateLibrary}.
*/
public static native String listUdfs(String libraryPath);
}
210 changes: 210 additions & 0 deletions common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.comet.udf

import scala.util.Try

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.functions.udf
import org.apache.spark.sql.types.{BinaryType, BooleanType, ByteType, DataType, DoubleType, FloatType, IntegerType, LongType, ShortType, StringType}

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode}

/**
* Public entry point for registering Rust scalar UDFs with Comet.
*
* See `docs/source/user-guide/latest/custom-rust-udfs.md` for an end-to-end walkthrough.
*/
object CometRustUDF {

/** Spark conf key under which registered Rust UDF entries are propagated to executors. */
val RUST_UDFS_CONF_KEY = "spark.comet.rustUdfs"

private val mapper: ObjectMapper = new ObjectMapper()

/**
* Register a single Rust UDF with an explicit signature.
*
* Validates the library on the driver, throws if loading fails or the declared signature
* differs from what the library reports. On success: a stub Spark catalog UDF is registered (so
* SQL/DataFrame reference checks pass), the driver-side registry is updated, and the conf key
* `spark.comet.rustUdfs` is updated so executors see the registration.
*/
def register(
spark: SparkSession,
name: String,
libraryPath: String,
inputTypes: Seq[DataType],
returnType: DataType,
deterministic: Boolean = true): Unit = {
val described = describeOne(libraryPath, name)
requireSignatureMatch(name, described, inputTypes, returnType)
installCatalogStub(spark, name, inputTypes, returnType, deterministic)
val meta = RustUdfMetadata(libraryPath, inputTypes, returnType, deterministic)
CometRustUdfRegistry.instance.register(name, meta)
propagateConf(spark)
Comment on lines +59 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't let registration override or collapse the library's volatility.

register() validates only the types and then trusts the caller's deterministic flag. registerAll() similarly reduces the descriptor's volatility to d.volatility != 2. That loses information from the native contract and allows the JVM side to plan a UDF with different determinism than the library reported. Preserve the descriptor's volatility end-to-end and reject mismatches instead of downgrading it here.

Also applies to: 76-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala` around lines
59 - 64, The current registration path (describeOne -> requireSignatureMatch ->
RustUdfMetadata -> CometRustUdfRegistry.instance.register and registerAll)
discards the native descriptor's volatility by trusting the caller's
deterministic flag or by computing volatility as d.volatility != 2; instead
preserve and enforce the descriptor volatility end-to-end: read the volatility
from the describeOne result (e.g., described.volatility), compare it against the
provided deterministic flag in register() and reject the registration if they
conflict (throw/require), and in registerAll() stop collapsing volatility to
d.volatility != 2 — propagate the exact descriptor volatility into
RustUdfMetadata (or fail on mismatch) so the JVM always uses the native-reported
volatility rather than downgrading it.

}

/**
* Register every UDF exposed by `libraryPath`, using each UDF's discovered signature. Returns
* the list of names registered.
*/
def registerAll(spark: SparkSession, libraryPath: String): Seq[String] = {
val descs = describeAll(libraryPath)
descs.map { d =>
val args = d.args.map(parseSparkType)
val ret = parseSparkType(d.returnType)
val deterministic = d.volatility != 2 // Volatile = non-deterministic
installCatalogStub(spark, d.name, args, ret, deterministic)
val meta = RustUdfMetadata(libraryPath, args, ret, deterministic)
CometRustUdfRegistry.instance.register(d.name, meta)
propagateConf(spark)
d.name
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerAll breaks on struct types

High Severity

registerAll maps JNI type strings through parseSparkType, which only handles a few Arrow primitive names and otherwise calls DataType.fromDDL. The bridge emits Arrow Debug/Display strings (e.g. for struct args), which Spark DDL cannot parse, so registration throws partway through the library. That breaks auto-discovery for non-primitive UDFs such as struct_field_a and contradicts CometRustUDFDriverSuite.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 548b7a5. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerAll leaves partial registrations

Medium Severity

registerAll registers each UDF inside a map that updates the global registry, Spark catalog stubs, and spark.comet.rustUdfs before moving to the next descriptor. If a later UDF fails (e.g. type parsing), earlier names stay registered and propagated even though the method throws and returns no successful list.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 548b7a5. Configure here.

Comment on lines +73 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling propagateConf(spark) inside the map loop is inefficient as it re-serializes the entire registry and updates the Spark configuration for every single UDF being registered. It should be called once after the loop completes.

    val names = descs.map { d =>
      val args = d.args.map(parseSparkType)
      val ret = parseSparkType(d.returnType)
      val deterministic = d.volatility != 2 // Volatile = non-deterministic
      installCatalogStub(spark, d.name, args, ret, deterministic)
      val meta = RustUdfMetadata(libraryPath, args, ret, deterministic)
      CometRustUdfRegistry.instance.register(d.name, meta)
      d.name
    }
    propagateConf(spark)
    names

}

// -------- internals --------

private case class Described(
name: String,
args: Seq[String],
returnType: String,
volatility: Int)

private def describeOne(libraryPath: String, name: String): Described = {
val json =
invokeBridge(() => CometRustUdfBridge.validateLibrary(libraryPath, name), libraryPath)
parseDescribed(json)
}

private def describeAll(libraryPath: String): Seq[Described] = {
val json = invokeBridge(() => CometRustUdfBridge.listUdfs(libraryPath), libraryPath)
val arr = mapper.readTree(json)
require(arr.isArray, s"listUdfs returned non-array JSON: $json")
val out = scala.collection.mutable.ArrayBuffer[Described]()
val it = arr.elements()
while (it.hasNext) out += parseDescribed(it.next().toString)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line performs redundant JSON parsing. it.next() already returns a JsonNode, which is then converted to a string and re-parsed in parseDescribed. Consider updating parseDescribed to accept a JsonNode directly to avoid this overhead.

out.toSeq
}

private def invokeBridge(call: () => String, libraryPath: String): String = {
Try(call()).recover { case t: Throwable => throw classifyNativeError(libraryPath, t) }.get
}

private def parseDescribed(json: String): Described = {
val node = mapper.readTree(json).asInstanceOf[ObjectNode]
val argsArr = node.get("args").asInstanceOf[ArrayNode]
val args = scala.collection.mutable.ArrayBuffer[String]()
val it = argsArr.elements()
while (it.hasNext) args += it.next().asText()
Described(
name = node.get("name").asText(),
args = args.toSeq,
returnType = node.get("return_type").asText(),
volatility = node.get("volatility").asInt())
}

private def classifyNativeError(libraryPath: String, t: Throwable): RuntimeException = {
val m = Option(t.getMessage).getOrElse("")
if (m.contains("ABI") || m.contains("missing required symbol")) {
new CometRustUdfAbiException(m)
} else if (m.contains("not found in")) {
new java.util.NoSuchElementException(m)
} else {
new CometRustUdfLoadException(s"failed to load $libraryPath: $m", t)
}
}

private def requireSignatureMatch(
name: String,
d: Described,
declaredArgs: Seq[DataType],
declaredReturn: DataType): Unit = {
val nativeArgs = d.args.map(parseSparkType)
val nativeReturn = parseSparkType(d.returnType)
if (nativeArgs != declaredArgs) {
throw new CometRustUdfSignatureException(
s"UDF '$name' arg types: native=$nativeArgs, declared=$declaredArgs")
}
if (nativeReturn != declaredReturn) {
throw new CometRustUdfSignatureException(
s"UDF '$name' return type: native=$nativeReturn, declared=$declaredReturn")
}
}

/** Parse a Comet-emitted Arrow type string into a Spark DataType. */
private def parseSparkType(s: String): DataType = s match {
case "Boolean" => BooleanType
case "Int8" => ByteType
case "Int16" => ShortType
case "Int32" => IntegerType
case "Int64" => LongType
case "Float32" => FloatType
case "Float64" => DoubleType
case "Utf8" | "LargeUtf8" => StringType
case "Binary" | "LargeBinary" => BinaryType
case other =>
// For complex/nested types: attempt to parse via Spark's DDL parser.
// The mapping is approximate for v1 — primitives are exact;
// nested types may need refinement in a follow-up.
DataType.fromDDL(other)
Comment on lines +165 to +169

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

DataType.fromDDL is likely to fail for complex types because the input string other comes from Arrow's DataType::to_string() in Rust, which does not follow Spark's DDL format for nested types like structs. This will prevent registration of UDFs with complex signatures even when using the explicit register method. A more robust mapping or a custom parser is needed for nested types.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unsigned and date mappings

Medium Severity

parseSparkType omits several Arrow primitives the SDK and loader already support (UInt8UInt64, Date32, Date64, Null). JNI descriptions use Arrow type strings for those values, so register/registerAll signature checks and auto-registration fail or mis-compare types even when the cdylib is valid.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 548b7a5. Configure here.

Comment on lines +137 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

What input syntax does Apache Spark's org.apache.spark.sql.types.DataType.fromDDLaccept, and are Apache Arrow RustDataType::to_string()outputs for complex types likeStruct(...), List(...), or Map(...) valid Spark DDL?

💡 Result:

Spark’s DataType.fromDDL(String ddl) accepts a Spark SQL schema DDL string that describes data types (including nested ones) using Spark’s DDL syntax. For StructType specifically, it’s a comma-separated list of field definitions, where each field definition is “name TYPE” (examples show field names in backticks, e.g. “a STRING, b INT”) [1][2]. More generally, DataType.fromDDL “parses data type from a string with schema” [3]. Are Apache Arrow Rust DataType::to_string outputs for complex types (Struct(...), List(...), Map(...)) valid Spark DDL for DataType.fromDDL? No—not in general. Arrow Rust’s DataType::to_string / Display outputs complex types using Arrow’s own wrapper/function-like names such as “Dictionary(Int32, Utf8)” and its Rust docs state Display/FromStr are reversible for Arrow types [4][5]. That string format is not Spark’s DDL format (Spark uses keywords like STRUCT<...>, ARRAY<...>, MAP<...> rather than Arrow’s “Struct(...)”, “List(...)”, “Map(...)” wrappers) [1][2]. Therefore Arrow Rust to_string output for nested types is not guaranteed to be accepted by Spark DataType.fromDDL. What to do instead: convert Arrow types to Spark’s DDL syntax with a mapping layer (e.g., Arrow Struct fields -> Spark STRUCT<...>, Arrow List element -> Spark ARRAY<...>, Arrow Map -> Spark MAP<keyType,valueType>), rather than passing Arrow’s to_string output directly into fromDDL.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Comet-emitted arrow type strings usage/production
rg -n "parseSparkType\\(" -S common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala
rg -n "UDF '.*' arg types|CometRustUdfSignatureException|returnType|args\\.map\\(parseSparkType\\)" -S common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala

# Search for "to_string" or "DataType" string emission on Rust side if present in repo
rg -n "to_string\\(\\)|Display|fromDDL\\(|STRUCT<|ARRAY<|MAP<|LargeUtf8|LargeBinary|Dictionary\\(" -S .

# Find any code that builds the type strings for UDF descriptors
rg -n "native.*type|declaredReturn|declaredArgs|descriptor.*type|signature.*type|returnType" -S .

Repository: martin-augment/datafusion-comet

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Comet-emitted arrow type strings usage/production
rg -n "parseSparkType\\(" -S common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala
rg -n "CometRustUdfSignatureException|returnType|args\\.map\\(parseSparkType\\)|declaredArgs|declaredReturn" -S common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala

# Search for where native/arrow type strings are produced/serialized
rg -n "to_string\\(\\)|DataType::to_string|FromStr|Display\\s*\\+|parse.*type|type_string|arrow.*type|struct\\(|list\\(|map\\(" -S .

# Search for spark DDL parsing usage
rg -n "fromDDL\\(" -S .

# Print nearby definitions for parseSparkType and requireSignatureMatch
sed -n '1,220p' common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala

Repository: martin-augment/datafusion-comet

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala"

# Show the relevant sections around requireSignatureMatch/parseSparkType and where `d` is parsed.
sed -n '1,220p' "$FILE" | sed -n '1,220p'
sed -n '220,420p' "$FILE"

# Locate the Described type definition and any JSON parsing for d.args / d.returnType
rg -n "Described" "$FILE"
rg -n "return_type|args\\b" "$FILE"
rg -n "node\\.get\\(\"return_type\"\\)|node\\.get\\(\"args\"\\)" "$FILE"

Repository: martin-augment/datafusion-comet

Length of output: 9327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala"

# Narrowly inspect around the signature parsing and where `d` is constructed
sed -n '90,220p' "$FILE"
sed -n '220,340p' "$FILE"

# Find the `Described` type definition and how `d.args` / `d.returnType` are parsed/constructed
rg -n "type Described|case class Described|class Described|Described\\b" "$FILE"
rg -n "return_type|args\\b" "$FILE"
rg -n "node\\.get\\(\"return_type\"\\)|node\\.get\\(\"args\"\\)" "$FILE"

Repository: martin-augment/datafusion-comet

Length of output: 5856


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "\"return_type\"|return_type\\\"|returnType\"|listUdfs\\(|validateLibrary\\(|CometRustUdfBridge" -S native common spark .

Repository: martin-augment/datafusion-comet

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the bridge implementation (Scala/JNI) that produces the JSON consumed by CometRustUDF.scala
rg -n "CometRustUdfBridge" -S .
rg -n "\"return_type\"" -S .
rg -n "\"args\"" -S common native spark . || true

# Also search for listUdfs/validateLibrary symbols
rg -n "listUdfs|validateLibrary" -S .

# If there are Rust functions, narrow further
rg -n "return_type|listUdfs|validateLibrary" -S native .

Repository: martin-augment/datafusion-comet

Length of output: 25136


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,140p' native/core/src/comet_rust_udf_bridge.rs

Repository: martin-augment/datafusion-comet

Length of output: 3851


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "return_type" native/core/src/comet_rust_udf_bridge.rs
rg -n "\"args\"|args" native/core/src/comet_rust_udf_bridge.rs
sed -n '1,120p' native/comet-udf-sdk/src/types.rs
sed -n '150,280p' native/comet-udf-sdk/src/types.rs || true

Repository: martin-augment/datafusion-comet

Length of output: 10015


Complex Rust UDF signatures won’t round-trip through this parser.

parseSparkType (in common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala) only maps a few primitive names, and for everything else calls DataType.fromDDL(other). However, the native bridge (native/core/src/comet_rust_udf_bridge.rs) serializes "args" and "return_type" using Arrow DataType::to_string() (udf.args.iter().map(|t| t.to_string()) / udf.return_type.to_string()), whose complex-type format isn’t Spark SQL DDL syntax—so fromDDL will fail or misparse for Struct/List/Map-style types, breaking register()/registerAll() signature validation.

Fix by implementing a real Arrow→Spark DataType conversion for complex/nested types (recursive mapping), or by emitting Spark-compatible DDL (or structured schema) from the native side instead of relying on string parsing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/src/main/scala/org/apache/comet/udf/CometRustUDF.scala` around lines
137 - 170, The parser fails for complex Arrow types because parseSparkType (used
by requireSignatureMatch) only handles primitives and falls back to
DataType.fromDDL, but the native side (comet_rust_udf_bridge.rs) emits Arrow's
DataType::to_string() format which is not Spark DDL. Fix by either: (A)
implement a recursive Arrow→Spark mapping inside parseSparkType to recognize
Arrow tokens like "Struct", "List"/"FixedSizeList", "Map", "Decimal", nested
"Field(name: type, ...)" patterns and construct corresponding Spark DataType
instances (StructType, ArrayType, MapType, DecimalType, etc.), ensuring
parseSparkType and requireSignatureMatch correctly validate signatures; or (B)
change comet_rust_udf_bridge.rs to emit Spark-compatible DDL or a structured
JSON schema for udf.args and udf.return_type and update parseSparkType to parse
that structured format instead. Ensure the chosen approach updates
parseSparkType and any native serialization code so string formats match
exactly.


private def installCatalogStub(
spark: SparkSession,
name: String,
inputTypes: Seq[DataType],
returnType: DataType,
deterministic: Boolean): Unit = {
val arity = inputTypes.size
val u: UserDefinedFunction = arity match {
case 0 =>
udf(() => throw new CometRustUdfNotEvaluatedException(name), returnType)
case 1 =>
udf((_: Any) => throw new CometRustUdfNotEvaluatedException(name), returnType)
case 2 =>
udf((_: Any, _: Any) => throw new CometRustUdfNotEvaluatedException(name), returnType)
case 3 =>
udf(
(_: Any, _: Any, _: Any) => throw new CometRustUdfNotEvaluatedException(name),
returnType)
case 4 =>
udf(
(_: Any, _: Any, _: Any, _: Any) => throw new CometRustUdfNotEvaluatedException(name),
returnType)
case n =>
throw new IllegalArgumentException(
s"Rust UDF '$name' arity $n not supported by stub. Reduce arity " +
s"or open a feature request to extend stub coverage.")
}
val finalUdf = if (deterministic) u else u.asNondeterministic()
spark.udf.register(name, finalUdf)
}

private def propagateConf(spark: SparkSession): Unit = {
val snapshot = CometRustUdfRegistry.instance.snapshot
val entries = snapshot.toSeq.map { case (name, meta) =>
mapper.writeValueAsString(java.util.Map.of("name", name, "libraryPath", meta.libraryPath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

java.util.Map.of was introduced in Java 9. To maintain compatibility with Java 8 (which is the standard for Spark extensions), please use a java.util.HashMap or another Java 8 compatible way to create the map.

      val map = new java.util.HashMap[String, String]()
      map.put("name", name)
      map.put("libraryPath", meta.libraryPath)
      mapper.writeValueAsString(map)

}
spark.conf.set(RUST_UDFS_CONF_KEY, entries.mkString(";"))

@augmentcode augmentcode Bot May 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spark.comet.rustUdfs is encoded as JSON entries joined by ;, which becomes ambiguous if libraryPath (or future fields) contains ;, making downstream parsing lossy. Consider a structured encoding (e.g., a JSON array) to avoid delimiter collisions.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.comet.udf

import org.apache.comet.{CometNativeException, CometRuntimeException}

/** Thrown when a Rust UDF dynamic library cannot be opened. */
class CometRustUdfLoadException(msg: String, cause: Throwable = null)
extends CometNativeException(msg) {
if (cause != null) initCause(cause)
}

/**
* Thrown when a Rust UDF library exposes the wrong ABI version or is missing required symbols.
*/
class CometRustUdfAbiException(msg: String) extends CometNativeException(msg)

/**
* Thrown when the declared signature does not match what the library reports via
* comet_udf_describe.
*/
class CometRustUdfSignatureException(msg: String) extends CometRuntimeException(msg)

/**
* Thrown by the catalog stub if a registered Rust UDF is invoked on the JVM (which means Comet's
* plan rule did not replace it).
*/
class CometRustUdfNotEvaluatedException(name: String)
extends CometRuntimeException(
s"Rust UDF '$name' must run inside Comet native execution; the JVM " +
s"stub was invoked, which means Comet did not replace this expression " +
s"with a native call. Check that Comet is enabled for the operator " +
s"hosting this expression.")
Loading