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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.calcite.adapter.enumerable;

import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.avatica.util.DateTimeUtils;
import org.apache.calcite.linq4j.AbstractEnumerable;
import org.apache.calcite.linq4j.Enumerable;
Expand Down Expand Up @@ -306,6 +307,8 @@ private static Expression toInternal(Expression operand,
} else if (targetType == Long.class) {
return Expressions.call(BuiltInMethod.TIMESTAMP_TO_LONG_OPTIONAL.method, operand);
}
} else if (fromType == byte[].class && targetType == ByteString.class) {
return Expressions.call(BuiltInMethod.BYTE_ARRAY_TO_BYTE_STRING.method, operand);
}
return operand;
}
Expand Down Expand Up @@ -346,6 +349,8 @@ private static Expression fromInternal(Expression operand,
if (isA(fromType, Primitive.LONG)) {
return Expressions.call(BuiltInMethod.INTERNAL_TO_TIMESTAMP.method, operand);
}
} else if (targetType == byte[].class && fromType == ByteString.class) {
return Expressions.call(BuiltInMethod.BYTE_STRING_TO_BYTE_ARRAY.method, operand);
}
if (Primitive.is(operand.type)
&& Primitive.isBox(targetType)) {
Expand Down Expand Up @@ -437,6 +442,13 @@ public static Expression convert(Expression operand, Type fromType,
return operand;
}

if (fromType == byte[].class && toType == ByteString.class) {
return Expressions.call(BuiltInMethod.BYTE_ARRAY_TO_BYTE_STRING.method, operand);
}
if (fromType == ByteString.class && toType == byte[].class) {
return Expressions.call(BuiltInMethod.BYTE_STRING_TO_BYTE_ARRAY.method, operand);
}

// TODO use Expressions#convertChecked to throw exception in case of overflow (CALCITE-6366)

// E.g. from "Short" to "int".
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -6010,6 +6010,22 @@ public static int time(long timestampMillis, String timeZone) {
}
}

public static @PolyNull ByteString byteArrayToByteString(byte @PolyNull [] bytes) {
if (bytes == null) {
return null;
} else {
return new ByteString(bytes);
}
}

public static byte @PolyNull [] byteStringToByteArray(@PolyNull ByteString s) {
if (s == null) {
return null;
} else {
return s.getBytes();
}
}

/** Helper for CAST(... AS VARBINARY(maxLength)). */
public static @PolyNull ByteString truncate(@PolyNull ByteString s, int maxLength) {
if (s == null) {
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,8 @@ public enum BuiltInMethod {
STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, "toTimestampWithLocalTimeZone",
String.class),
STRING_TO_BINARY(SqlFunctions.class, "stringToBinary", String.class, Charset.class),
BYTE_ARRAY_TO_BYTE_STRING(SqlFunctions.class, "byteArrayToByteString", byte[].class),
BYTE_STRING_TO_BYTE_ARRAY(SqlFunctions.class, "byteStringToByteArray", ByteString.class),
TIMESTAMP_STRING_TO_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlFunctions.class,
"toTimestampWithLocalTimeZone", String.class, TimeZone.class),
TIME_WITH_LOCAL_TIME_ZONE_TO_TIME(SqlFunctions.class, "timeWithLocalTimeZoneToTime",
Expand Down
35 changes: 35 additions & 0 deletions core/src/test/java/org/apache/calcite/test/UdfTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ private CalciteAssert.AssertThat withUdf() {
+ "'\n"
+ " },\n"
+ " {\n"
+ " name: 'BYTEARRAY',\n"
+ " className: '"
+ Smalls.ByteArrayFunction.class.getName()
+ "'\n"
+ " },\n"
+ " {\n"
+ " name: 'BYTEARRAY_LENGTH',\n"
+ " className: '"
+ Smalls.ByteArrayLengthFunction.class.getName()
+ "'\n"
+ " },\n"
+ " {\n"
+ " name: 'CHARACTERARRAY',\n"
+ " className: '"
+ Smalls.CharacterArrayFunction.class.getName()
Expand Down Expand Up @@ -1107,6 +1119,29 @@ private static CalciteAssert.AssertThat withBadUdf(Class<?> clazz) {
withUdf().query(sql2).returns("C=true\n");
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-7187">[CALCITE-7187]
* Java UDF byte arrays cannot be mapped to VARBINARY</a>. */
@Test void testByteArrayDirectComparison() {
final String testString = "test";
final String testHex = "74657374";

final String sql = "values \"adhoc\".bytearray('" + testString + "')";
withUdf().query(sql).typeIs("[EXPR$0 VARBINARY]");

final String sql2 = "select \"adhoc\".bytearray(cast('" + testString
+ "' as varchar)) = x'" + testHex + "' as C\n";
withUdf().query(sql2).returns("C=true\n");
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-7187">[CALCITE-7187]
* Java UDF byte arrays cannot be mapped to VARBINARY</a>. */
@Test void testByteArrayParameter() {
withUdf().query("values \"adhoc\".bytearray_length(x'74657374')")
.returns("EXPR$0=4\n");
}

/**
* Test for <a href="https://issues.apache.org/jira/browse/CALCITE-7186">[CALCITE-7186]</a>
* Add mapping from Character[] to VARCHAR in Java UDF.
Expand Down
4 changes: 4 additions & 0 deletions site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3426,6 +3426,10 @@ can specify the name and optionality of each parameter using the
[Parameter]({{ site.apiRoot }}/org/apache/calcite/linq4j/function/Parameter.html)
annotation.

For Java UDFs, `byte[]` and `ByteString` are supported Java representations of
SQL `VARBINARY` values for parameters and return types. Boxed byte arrays
(`Byte[]`) are not supported.
Comment on lines +3429 to +3431
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The doc mentions ByteString without qualification; since there are multiple ByteString types in the Java ecosystem, consider using the fully qualified name (org.apache.calcite.avatica.util.ByteString) or linking to the API docs to avoid ambiguity. Also, the sentence about Byte[] could be misread as "not supported at all"; if the intent is "not supported as a VARBINARY representation", consider stating that explicitly.

Suggested change
For Java UDFs, `byte[]` and `ByteString` are supported Java representations of
SQL `VARBINARY` values for parameters and return types. Boxed byte arrays
(`Byte[]`) are not supported.
For Java UDFs, `byte[]` and [`ByteString`]({{ site.apiRoot }}/org/apache/calcite/avatica/util/ByteString.html) are supported Java representations of
SQL `VARBINARY` values for parameters and return types. Boxed byte arrays
(`Byte[]`) are not supported as Java representations of SQL `VARBINARY` parameters or return types.

Copilot uses AI. Check for mistakes.

### Calling functions with named and optional parameters

Usually when you call a function, you need to specify all of its parameters,
Expand Down
18 changes: 18 additions & 0 deletions testkit/src/main/java/org/apache/calcite/util/Smalls.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
Expand Down Expand Up @@ -1493,6 +1494,23 @@ public static ByteString eval(String s) {
}
}

/** User-defined function with return type byte[]. */
public static class ByteArrayFunction {
public static byte[] eval(String s) {
if (s == null) {
return null;
}
return s.getBytes(StandardCharsets.UTF_8);
}
}

/** User-defined function with parameter type byte[]. */
public static class ByteArrayLengthFunction {
public static int eval(byte[] bytes) {
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

ByteArrayLengthFunction.eval does not handle null input; if a SQL VARBINARY argument is NULL (or conversion yields null), this will throw a NullPointerException. Consider changing the signature to return a boxed Integer and return null when bytes is null (or otherwise define/implement the desired NULL semantics).

Suggested change
public static int eval(byte[] bytes) {
public static @Nullable Integer eval(@Nullable byte[] bytes) {
if (bytes == null) {
return null;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

UDF in Smalls is just for test

return bytes.length;
}
}

/** User-defined function with return type Character[]. */
public static class CharacterArrayFunction {
public static Character[] eval(String s) {
Expand Down
Loading