From 5d8b728956ca65ecf97fa20ce124fb935d26ed14 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Tue, 9 Jun 2026 16:46:59 +0100 Subject: [PATCH] Expose Merge expression in vortex-jni Signed-off-by: Robert Kruszewski --- .../main/java/dev/vortex/api/Expression.java | 40 +++++++++++++++++++ .../java/dev/vortex/jni/NativeExpression.java | 2 + .../java/dev/vortex/api/ExpressionTest.java | 11 +++++ vortex-jni/src/expression.rs | 31 ++++++++++++++ 4 files changed, 84 insertions(+) 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 { TimeUnit::try_from(tag as u8).map_err(JNIError::from) } +/// Parse a merge [`DuplicateHandling`] strategy from its wire-encoded byte tag. +/// +/// See `dev.vortex.api.Expression.DuplicateHandling` on the Java side for the source of truth. +fn parse_duplicate_handling(tag: jbyte) -> Result { + Ok(match tag { + 0 => DuplicateHandling::RightMost, + 1 => DuplicateHandling::Error, + other => throw_runtime!("unknown duplicate handling code: {other}"), + }) +} + #[unsafe(no_mangle)] pub extern "system" fn Java_dev_vortex_jni_NativeExpression_free( _env: EnvUnowned, @@ -192,6 +205,24 @@ pub extern "system" fn Java_dev_vortex_jni_NativeExpression_pack( }) } +/// Merge zero or more struct-returning expressions into a single struct. +/// +/// `duplicate_handling` selects how shared field names are resolved (see +/// [`parse_duplicate_handling`]). An empty `expressions` array yields an empty struct. +#[unsafe(no_mangle)] +pub extern "system" fn Java_dev_vortex_jni_NativeExpression_merge( + mut env: EnvUnowned, + _class: JClass, + expressions: JLongArray, + duplicate_handling: jbyte, +) -> jlong { + try_or_throw(&mut env, |env| { + let exprs = collect_operands(env, &expressions)?; + let handling = parse_duplicate_handling(duplicate_handling)?; + Ok(into_raw(merge_opts(exprs, handling))) + }) +} + #[unsafe(no_mangle)] pub extern "system" fn Java_dev_vortex_jni_NativeExpression_and( mut env: EnvUnowned,