Skip to content
Merged
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
40 changes: 40 additions & 0 deletions java/vortex-jni/src/main/java/dev/vortex/api/Expression.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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 <em>not</em> 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");
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions java/vortex-jni/src/test/java/dev/vortex/api/ExpressionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
31 changes: 31 additions & 0 deletions vortex-jni/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -97,6 +99,17 @@ fn parse_time_unit(tag: jbyte) -> Result<TimeUnit, JNIError> {
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<DuplicateHandling, JNIError> {
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,
Expand Down Expand Up @@ -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,
Expand Down
Loading