Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,21 @@ add_library(duckdb_java SHARED
src/jni/bindings_common.cpp
src/jni/bindings_data_chunk.cpp
src/jni/bindings_logical_type.cpp
src/jni/bindings_scalar_function.cpp
src/jni/bindings_table_function.cpp
src/jni/bindings_validity.cpp
src/jni/bindings_vector.cpp
src/jni/config.cpp
src/jni/duckdb_java.cpp
src/jni/functions.cpp
src/jni/refs.cpp
src/jni/types.cpp
src/jni/udf_callbacks.cpp
src/jni/udf_registration.cpp
src/jni/udf_registration_impl.cpp
src/jni/udf_table_bind_conversion.cpp
src/jni/udf_types.cpp
src/jni/udf_vector_accessors.cpp
src/jni/util.cpp
${DUCKDB_SRC_FILES})

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ This optionally takes an argument to only run a single test, for example:
```
java -cp "build/release/duckdb_jdbc_tests.jar:build/release/duckdb_jdbc.jar" org/duckdb/TestDuckDBJDBC test_valid_but_local_config_throws_exception
```

### User-Defined Functions (Java)

All Java UDF documentation and examples are available in [UDF.MD](UDF.MD).
212 changes: 212 additions & 0 deletions UDF.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# User-Defined Functions (Java)

This guide shows how to use Java Scalar UDFs and Table Functions with `DuckDBConnection`.

## Scalar UDF

Scalar UDF callbacks use a vectorized contract:

```java
ScalarUdf.apply(UdfContext ctx, UdfReader[] args, UdfScalarWriter out, int rowCount)
```

Use `rowCount` loops and write one output value per row.

### Basic example

```java
try (DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class);
Statement stmt = conn.createStatement()) {

conn.registerScalarUdf("add_one", DuckDBColumnType.INTEGER, DuckDBColumnType.INTEGER,
(ctx, args, out, rowCount) -> {
for (int row = 0; row < rowCount; row++) {
out.setInt(row, args[0].getInt(row) + 1);
}
});

try (ResultSet rs = stmt.executeQuery("SELECT add_one(41)")) {
rs.next();
System.out.println(rs.getInt(1)); // 42
}
}
```

### Registration forms

You can register scalar UDFs with:

- `DuckDBColumnType` signatures (`registerScalarUdf`)
- `Class<?>` signatures (`registerScalarUdf`)
- explicit `UdfLogicalType` signatures (`registerScalarUdf`)
- varargs signatures (`registerScalarUdfVarArgs`)

For decimal precision/scale, prefer explicit logical types:

```java
conn.registerScalarUdf(
"mul_decimal",
new UdfLogicalType[] {UdfLogicalType.decimal(20, 4), UdfLogicalType.decimal(20, 4)},
UdfLogicalType.decimal(38, 8),
(ctx, args, out, rowCount) -> {
for (int row = 0; row < rowCount; row++) {
out.setBigDecimal(row, args[0].getBigDecimal(row).multiply(args[1].getBigDecimal(row)));
}
}
);
```

### Options

`UdfOptions` controls scalar behavior:

- `deterministic(true|false)`: marks whether equal inputs always produce equal output. Use `false` for non-deterministic logic (for example random/time-based behavior).
- `nullSpecialHandling(true|false)`: when `true`, your callback receives rows that contain `NULL` input values; when `false`, DuckDB handles null propagation before callback execution.
- `returnNullOnException(true|false)`: when `true`, Java exceptions in callback rows are returned as `NULL`; when `false`, the query fails with an error.
- `varArgs(true|false)`: enables varargs registration (normally used via `registerScalarUdfVarArgs`).

Example:

```java
UdfOptions options = new UdfOptions()
.deterministic(true)
.nullSpecialHandling(true)
.returnNullOnException(false);

conn.registerScalarUdf("safe_add", DuckDBColumnType.INTEGER, DuckDBColumnType.INTEGER,
(ctx, args, out, rowCount) -> {
for (int row = 0; row < rowCount; row++) {
if (args[0].isNull(row)) {
out.setNull(row);
} else {
out.setInt(row, args[0].getInt(row) + 1);
}
}
}, options);
```

## UdfReader / UdfScalarWriter object mappings

| DuckDB type | Reader object | Writer object |
| --- | --- | --- |
| `BOOLEAN` | `Boolean` | `Boolean` |
| `TINYINT`, `SMALLINT`, `INTEGER`, `UTINYINT`, `USMALLINT` | `Integer` | `Integer` |
| `BIGINT`, `UINTEGER`, `UBIGINT` | `Long` | `Long` |
| `FLOAT` | `Float` | `Float` |
| `DOUBLE` | `Double` | `Double` |
| `DECIMAL` | `BigDecimal` | `BigDecimal` |
| `VARCHAR` | `String` | `String` |
| `BLOB` | `byte[]` | `byte[]` |
| `DATE` | `LocalDate` or `Date` | `LocalDate` or `Date` |
| `TIME`, `TIME_NS` | `LocalTime` | `LocalTime` |
| `TIME_WITH_TIME_ZONE` | `OffsetTime` | `OffsetTime` |
| `TIMESTAMP`, `TIMESTAMP_S`, `TIMESTAMP_MS`, `TIMESTAMP_NS` | `LocalDateTime` | `LocalDateTime` or `Date` |
| `TIMESTAMP_WITH_TIME_ZONE` | `OffsetDateTime` | `OffsetDateTime` or `Date` |
| `UUID` | `UUID` | `UUID` |
| `HUGEINT`, `UHUGEINT` | `byte[]` | `byte[]` |

`UdfScalarWriter` supports explicit setters and `setObject(...)`.

## Table Function

Table function callbacks use:

- `bind(BindContext ctx, Object[] parameters) -> TableBindResult`
- `init(InitContext ctx, TableBindResult bind) -> TableState`
- `produce(TableState state, UdfOutputAppender out) -> int`

What each callback does:

- `bind`: runs once per invocation to validate/interpret parameters, define output schema, and create bind state.
- `init`: runs after bind to initialize execution state (cursor/counters/chunk state).
- `produce`: runs repeatedly to emit rows in chunks; return the number of rows produced in that call.

### Basic example

```java
conn.registerTableFunction(
"range_java",
new TableFunction() {
@Override
public TableBindResult bind(BindContext ctx, Object[] parameters) {
long end = ((Number) parameters[0]).longValue();
return new TableBindResult(
new String[] {"i"},
new UdfLogicalType[] {UdfLogicalType.of(DuckDBColumnType.BIGINT)},
new long[] {0L, end}
);
}

@Override
public TableState init(InitContext ctx, TableBindResult bind) {
return new TableState(bind.getBindState());
}

@Override
public int produce(TableState state, UdfOutputAppender out) {
long[] st = (long[]) state.getState();
long current = st[0];
long end = st[1];
int produced = 0;

while (produced < 256 && current < end) {
out.beginRow().append(current).endRow();
current++;
produced++;
}

st[0] = current;
return produced;
}
},
new TableFunctionDefinition().withParameterTypes(new DuckDBColumnType[] {DuckDBColumnType.BIGINT}),
new TableFunctionOptions().threadSafe(false).maxThreads(1)
);
```

### Bind parameter object mappings

In `bind`, parameters are materialized as Java objects. Common mappings:

- `DECIMAL -> BigDecimal`
- `DATE -> LocalDate`
- `TIME`, `TIME_NS -> LocalTime`
- `TIMESTAMP* -> LocalDateTime`
- `TIME_WITH_TIME_ZONE -> OffsetTime`
- `TIMESTAMP_WITH_TIME_ZONE -> OffsetDateTime`
- `UUID -> UUID`

### Output writing with UdfOutputAppender

`UdfOutputAppender` supports:

- primitive/object `append(...)` for one column at a time
- `setObject(...)` and typed setters (`setBigDecimal`, `setLocalDate`, etc.)
- nested output objects for container types:
- `LIST`/`ARRAY`: Java arrays or `Collection`
- `MAP`: `Map`
- `STRUCT`: positional `List`/array or named `Map<String, Object>`
- `UNION`: `AbstractMap.SimpleEntry<String, Object>`
- `ENUM`: `String`

## Table function options

`TableFunctionOptions`:

- `threadSafe(false|true)`
- `maxThreads(int >= 1)`

`TableFunctionDefinition`:

- `withParameterTypes(...)`
- `withProjectionPushdown(true|false)`

## Unsupported in scalar signatures

Scalar UDF signatures do not support nested/container logical types (`LIST`, `STRUCT`, `MAP`, `ARRAY`, `UNION`, `ENUM`) and `INTERVAL`.

## Practical recommendations

- Use chunk-oriented loops (`rowCount`) for scalar UDF throughput.
- Avoid executing SQL on the same `DuckDBConnection` from inside callbacks.
- Use explicit logical types for decimal-sensitive workloads.
39 changes: 38 additions & 1 deletion duckdb_java.def
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1catalog
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1schema
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1startup

Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size
Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type
Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id
Expand Down Expand Up @@ -82,6 +81,12 @@ Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size
Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve
Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child
Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1varchar_1bytes
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1varchar_1bytes
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1blob_1bytes
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1blob_1bytes
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1decimal
Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1decimal
Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk
Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk
Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset
Expand All @@ -98,6 +103,38 @@ Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count
Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type
Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk
Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk
Java_org_duckdb_DuckDBBindings_duckdb_1create_1scalar_1function
Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1scalar_1function
Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1name
Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1add_1parameter
Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1return_1type
Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1volatile
Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1special_1handling
Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function
Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function_1java
Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function_1java_1with_1function
Java_org_duckdb_DuckDBBindings_duckdb_1create_1table_1function
Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1table_1function
Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1set_1name
Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1add_1parameter
Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1supports_1projection_1pushdown
Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function
Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function_1java
Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function_1java_1with_1function
Java_org_duckdb_DuckDBBindings_duckdb_1bind_1get_1parameter_1count
Java_org_duckdb_DuckDBBindings_duckdb_1bind_1get_1parameter
Java_org_duckdb_DuckDBBindings_duckdb_1bind_1add_1result_1column
Java_org_duckdb_DuckDBBindings_duckdb_1bind_1set_1bind_1data
Java_org_duckdb_DuckDBBindings_duckdb_1bind_1set_1error
Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1init_1data
Java_org_duckdb_DuckDBBindings_duckdb_1init_1get_1column_1count
Java_org_duckdb_DuckDBBindings_duckdb_1init_1get_1column_1index
Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1max_1threads
Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1error
Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1bind_1data
Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1init_1data
Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1local_1init_1data
Java_org_duckdb_DuckDBBindings_duckdb_1function_1set_1error

duckdb_adbc_init
duckdb_add_aggregate_function_to_set
Expand Down
39 changes: 38 additions & 1 deletion duckdb_java.exp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1catalog
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1schema
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1startup

_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size
_Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type
_Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id
Expand Down Expand Up @@ -79,6 +78,12 @@ _Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size
_Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve
_Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child
_Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1varchar_1bytes
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1varchar_1bytes
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1blob_1bytes
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1blob_1bytes
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1get_1decimal
_Java_org_duckdb_DuckDBBindings_duckdb_1udf_1set_1decimal
_Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk
_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk
_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset
Expand All @@ -95,6 +100,38 @@ _Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count
_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type
_Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk
_Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk
_Java_org_duckdb_DuckDBBindings_duckdb_1create_1scalar_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1scalar_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1name
_Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1add_1parameter
_Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1return_1type
_Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1volatile
_Java_org_duckdb_DuckDBBindings_duckdb_1scalar_1function_1set_1special_1handling
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function_1java
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1scalar_1function_1java_1with_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1create_1table_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1table_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1set_1name
_Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1add_1parameter
_Java_org_duckdb_DuckDBBindings_duckdb_1table_1function_1supports_1projection_1pushdown
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function_1java
_Java_org_duckdb_DuckDBBindings_duckdb_1register_1table_1function_1java_1with_1function
_Java_org_duckdb_DuckDBBindings_duckdb_1bind_1get_1parameter_1count
_Java_org_duckdb_DuckDBBindings_duckdb_1bind_1get_1parameter
_Java_org_duckdb_DuckDBBindings_duckdb_1bind_1add_1result_1column
_Java_org_duckdb_DuckDBBindings_duckdb_1bind_1set_1bind_1data
_Java_org_duckdb_DuckDBBindings_duckdb_1bind_1set_1error
_Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1init_1data
_Java_org_duckdb_DuckDBBindings_duckdb_1init_1get_1column_1count
_Java_org_duckdb_DuckDBBindings_duckdb_1init_1get_1column_1index
_Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1max_1threads
_Java_org_duckdb_DuckDBBindings_duckdb_1init_1set_1error
_Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1bind_1data
_Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1init_1data
_Java_org_duckdb_DuckDBBindings_duckdb_1function_1get_1local_1init_1data
_Java_org_duckdb_DuckDBBindings_duckdb_1function_1set_1error

_duckdb_adbc_init
_duckdb_add_aggregate_function_to_set
Expand Down
Loading
Loading