Skip to content

Commit 5d8b728

Browse files
committed
Expose Merge expression in vortex-jni
Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent 096f73e commit 5d8b728

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

java/vortex-jni/src/main/java/dev/vortex/api/Expression.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ public static Expression pack(String[] fieldNames, Expression[] expressions, boo
8484
return new Expression(NativeExpression.pack(fieldNames, nativePointers(expressions), nullable));
8585
}
8686

87+
/**
88+
* Merge struct expressions into a single struct, combining their fields in order.
89+
*
90+
* <p>Every input must evaluate to a non-nullable struct. When the same field name appears in more than one input,
91+
* {@code duplicateHandling} decides the result. Fields are <em>not</em> merged recursively — a later field replaces
92+
* an earlier one with the same name. Merging zero expressions yields an empty struct.
93+
*/
94+
public static Expression merge(DuplicateHandling duplicateHandling, Expression... expressions) {
95+
return new Expression(NativeExpression.merge(nativePointers(expressions), duplicateHandling.tag()));
96+
}
97+
98+
/**
99+
* Merge struct expressions, failing if any field name is duplicated. Equivalent to {@link #merge(DuplicateHandling,
100+
* Expression...)} with {@link DuplicateHandling#ERROR}.
101+
*/
102+
public static Expression merge(Expression... expressions) {
103+
return merge(DuplicateHandling.ERROR, expressions);
104+
}
105+
87106
/** Logical AND. Requires at least one operand. */
88107
public static Expression and(Expression... operands) {
89108
Preconditions.checkArgument(operands.length > 0, "and requires at least one operand");
@@ -292,6 +311,27 @@ public byte code() {
292311
}
293312
}
294313

314+
/**
315+
* Strategy for resolving duplicate field names in {@link #merge(DuplicateHandling, Expression...)}. Tag values must
316+
* match the Rust {@code parse_duplicate_handling} table.
317+
*/
318+
public enum DuplicateHandling {
319+
/** When two structs share a field name, keep the value from the right-most (later) struct. */
320+
RIGHT_MOST((byte) 0),
321+
/** When two structs share a field name, fail with an error. */
322+
ERROR((byte) 1);
323+
324+
private final byte tag;
325+
326+
DuplicateHandling(byte tag) {
327+
this.tag = tag;
328+
}
329+
330+
public byte tag() {
331+
return tag;
332+
}
333+
}
334+
295335
/** Time units for Date/Timestamp literals. Tag values must match the Rust {@code parse_time_unit} table. */
296336
public enum TimeUnit {
297337
NANOSECONDS((byte) 0),

java/vortex-jni/src/main/java/dev/vortex/jni/NativeExpression.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ private NativeExpression() {}
2121

2222
public static native long pack(String[] fieldNames, long[] expressions, boolean nullable);
2323

24+
public static native long merge(long[] expressions, byte duplicateHandling);
25+
2426
public static native long and(long[] operandPointers);
2527

2628
public static native long or(long[] operandPointers);

java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,15 @@ public void packComposes() {
4444
new Expression[] {Expression.column("a"), Expression.literal(5L), Expression.rowIdx()},
4545
true));
4646
}
47+
48+
@Test
49+
public void mergeComposes() {
50+
// Default duplicate handling (ERROR).
51+
assertNotNull(Expression.merge(Expression.column("a"), Expression.column("b")));
52+
// Explicit duplicate handling.
53+
assertNotNull(Expression.merge(
54+
Expression.DuplicateHandling.RIGHT_MOST, Expression.column("a"), Expression.column("b")));
55+
// Merging zero expressions is valid and yields an empty struct.
56+
assertNotNull(Expression.merge());
57+
}
4758
}

vortex-jni/src/expression.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use vortex::expr::get_item;
3939
use vortex::expr::is_not_null;
4040
use vortex::expr::is_null;
4141
use vortex::expr::lit;
42+
use vortex::expr::merge_opts;
4243
use vortex::expr::not;
4344
use vortex::expr::or_collect;
4445
use vortex::expr::pack;
@@ -59,6 +60,7 @@ use vortex::scalar_fn::fns::between::StrictComparison;
5960
use vortex::scalar_fn::fns::binary::Binary;
6061
use vortex::scalar_fn::fns::like::Like;
6162
use vortex::scalar_fn::fns::like::LikeOptions;
63+
use vortex::scalar_fn::fns::merge::DuplicateHandling;
6264
use vortex::scalar_fn::fns::operators::Operator;
6365

6466
use crate::errors::JNIError;
@@ -97,6 +99,17 @@ fn parse_time_unit(tag: jbyte) -> Result<TimeUnit, JNIError> {
9799
TimeUnit::try_from(tag as u8).map_err(JNIError::from)
98100
}
99101

102+
/// Parse a merge [`DuplicateHandling`] strategy from its wire-encoded byte tag.
103+
///
104+
/// See `dev.vortex.api.Expression.DuplicateHandling` on the Java side for the source of truth.
105+
fn parse_duplicate_handling(tag: jbyte) -> Result<DuplicateHandling, JNIError> {
106+
Ok(match tag {
107+
0 => DuplicateHandling::RightMost,
108+
1 => DuplicateHandling::Error,
109+
other => throw_runtime!("unknown duplicate handling code: {other}"),
110+
})
111+
}
112+
100113
#[unsafe(no_mangle)]
101114
pub extern "system" fn Java_dev_vortex_jni_NativeExpression_free(
102115
_env: EnvUnowned,
@@ -192,6 +205,24 @@ pub extern "system" fn Java_dev_vortex_jni_NativeExpression_pack(
192205
})
193206
}
194207

208+
/// Merge zero or more struct-returning expressions into a single struct.
209+
///
210+
/// `duplicate_handling` selects how shared field names are resolved (see
211+
/// [`parse_duplicate_handling`]). An empty `expressions` array yields an empty struct.
212+
#[unsafe(no_mangle)]
213+
pub extern "system" fn Java_dev_vortex_jni_NativeExpression_merge(
214+
mut env: EnvUnowned,
215+
_class: JClass,
216+
expressions: JLongArray,
217+
duplicate_handling: jbyte,
218+
) -> jlong {
219+
try_or_throw(&mut env, |env| {
220+
let exprs = collect_operands(env, &expressions)?;
221+
let handling = parse_duplicate_handling(duplicate_handling)?;
222+
Ok(into_raw(merge_opts(exprs, handling)))
223+
})
224+
}
225+
195226
#[unsafe(no_mangle)]
196227
pub extern "system" fn Java_dev_vortex_jni_NativeExpression_and(
197228
mut env: EnvUnowned,

0 commit comments

Comments
 (0)