diff --git a/java/vortex-jni/src/main/java/dev/vortex/api/Expression.java b/java/vortex-jni/src/main/java/dev/vortex/api/Expression.java index a228aff605f..b2e2a8be875 100644 --- a/java/vortex-jni/src/main/java/dev/vortex/api/Expression.java +++ b/java/vortex-jni/src/main/java/dev/vortex/api/Expression.java @@ -84,6 +84,25 @@ public static Expression pack(String[] fieldNames, Expression[] expressions, boo return new Expression(NativeExpression.pack(fieldNames, nativePointers(expressions), nullable)); } + /** + * Merge struct expressions into a single struct, combining their fields in order. + * + *
Every input must evaluate to a non-nullable struct. When the same field name appears in more than one input,
+ * {@code duplicateHandling} decides the result. Fields are not merged recursively — a later field replaces
+ * an earlier one with the same name. Merging zero expressions yields an empty struct.
+ */
+ public static Expression merge(DuplicateHandling duplicateHandling, Expression... expressions) {
+ return new Expression(NativeExpression.merge(nativePointers(expressions), duplicateHandling.tag()));
+ }
+
+ /**
+ * Merge struct expressions, failing if any field name is duplicated. Equivalent to {@link #merge(DuplicateHandling,
+ * Expression...)} with {@link DuplicateHandling#ERROR}.
+ */
+ public static Expression merge(Expression... expressions) {
+ return merge(DuplicateHandling.ERROR, expressions);
+ }
+
/** Logical AND. Requires at least one operand. */
public static Expression and(Expression... operands) {
Preconditions.checkArgument(operands.length > 0, "and requires at least one operand");
@@ -292,6 +311,27 @@ public byte code() {
}
}
+ /**
+ * Strategy for resolving duplicate field names in {@link #merge(DuplicateHandling, Expression...)}. Tag values must
+ * match the Rust {@code parse_duplicate_handling} table.
+ */
+ public enum DuplicateHandling {
+ /** When two structs share a field name, keep the value from the right-most (later) struct. */
+ RIGHT_MOST((byte) 0),
+ /** When two structs share a field name, fail with an error. */
+ ERROR((byte) 1);
+
+ private final byte tag;
+
+ DuplicateHandling(byte tag) {
+ this.tag = tag;
+ }
+
+ public byte tag() {
+ return tag;
+ }
+ }
+
/** Time units for Date/Timestamp literals. Tag values must match the Rust {@code parse_time_unit} table. */
public enum TimeUnit {
NANOSECONDS((byte) 0),
diff --git a/java/vortex-jni/src/main/java/dev/vortex/jni/NativeExpression.java b/java/vortex-jni/src/main/java/dev/vortex/jni/NativeExpression.java
index 985687371e3..bcd82d4b313 100644
--- a/java/vortex-jni/src/main/java/dev/vortex/jni/NativeExpression.java
+++ b/java/vortex-jni/src/main/java/dev/vortex/jni/NativeExpression.java
@@ -21,6 +21,8 @@ private NativeExpression() {}
public static native long pack(String[] fieldNames, long[] expressions, boolean nullable);
+ public static native long merge(long[] expressions, byte duplicateHandling);
+
public static native long and(long[] operandPointers);
public static native long or(long[] operandPointers);
diff --git a/java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java b/java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java
index c33e6901278..e02024311d8 100644
--- a/java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java
+++ b/java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java
@@ -44,4 +44,15 @@ public void packComposes() {
new Expression[] {Expression.column("a"), Expression.literal(5L), Expression.rowIdx()},
true));
}
+
+ @Test
+ public void mergeComposes() {
+ // Default duplicate handling (ERROR).
+ assertNotNull(Expression.merge(Expression.column("a"), Expression.column("b")));
+ // Explicit duplicate handling.
+ assertNotNull(Expression.merge(
+ Expression.DuplicateHandling.RIGHT_MOST, Expression.column("a"), Expression.column("b")));
+ // Merging zero expressions is valid and yields an empty struct.
+ assertNotNull(Expression.merge());
+ }
}
diff --git a/vortex-jni/src/expression.rs b/vortex-jni/src/expression.rs
index 321717b44ed..77d709ddd38 100644
--- a/vortex-jni/src/expression.rs
+++ b/vortex-jni/src/expression.rs
@@ -39,6 +39,7 @@ use vortex::expr::get_item;
use vortex::expr::is_not_null;
use vortex::expr::is_null;
use vortex::expr::lit;
+use vortex::expr::merge_opts;
use vortex::expr::not;
use vortex::expr::or_collect;
use vortex::expr::pack;
@@ -59,6 +60,7 @@ use vortex::scalar_fn::fns::between::StrictComparison;
use vortex::scalar_fn::fns::binary::Binary;
use vortex::scalar_fn::fns::like::Like;
use vortex::scalar_fn::fns::like::LikeOptions;
+use vortex::scalar_fn::fns::merge::DuplicateHandling;
use vortex::scalar_fn::fns::operators::Operator;
use crate::errors::JNIError;
@@ -97,6 +99,17 @@ fn parse_time_unit(tag: jbyte) -> Result