diff --git a/api/src/main/java/org/apache/iceberg/expressions/Expressions.java b/api/src/main/java/org/apache/iceberg/expressions/Expressions.java index 22626866725b..ba1b5ad1cb52 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Expressions.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Expressions.java @@ -329,6 +329,36 @@ public static Literal lit(T value) { return Literals.from(value); } + /** + * Create a {@link Literal} from a microsecond value. + * + * @param micros a timestamp in microseconds from the unix epoch + * @return a literal representing the timestamp + */ + public static Literal micros(long micros) { + return new Literals.TimestampLiteral(micros); + } + + /** + * Create a {@link Literal} from a millisecond value. + * + * @param millis a timestamp in milliseconds from the unix epoch + * @return a literal representing the timestamp + */ + public static Literal millis(long millis) { + return new Literals.TimestampLiteral(millis * 1000); + } + + /** + * Create a {@link Literal} from a nanosecond value. + * + * @param nanos a timestamp in nanoseconds from the unix epoch + * @return a literal representing the timestamp + */ + public static Literal nanos(long nanos) { + return new Literals.TimestampNanoLiteral(nanos); + } + public static UnboundAggregate count(String name) { return new UnboundAggregate<>(Operation.COUNT, ref(name)); } diff --git a/api/src/main/java/org/apache/iceberg/expressions/Literals.java b/api/src/main/java/org/apache/iceberg/expressions/Literals.java index ee47035b1e72..55313be391b3 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Literals.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Literals.java @@ -60,7 +60,9 @@ static Literal from(T value) { Preconditions.checkNotNull(value, "Cannot create expression literal from null"); Preconditions.checkArgument(!NaNUtil.isNaN(value), "Cannot create expression literal from NaN"); - if (value instanceof Boolean) { + if (value instanceof Literal) { + return (Literal) value; + } else if (value instanceof Boolean) { return (Literal) new Literals.BooleanLiteral((Boolean) value); } else if (value instanceof Integer) { return (Literal) new Literals.IntegerLiteral((Integer) value); diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java index 8bb03c633ab5..2a1fab10a445 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java @@ -47,7 +47,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; import java.util.concurrent.Callable; +import org.apache.iceberg.Schema; import org.apache.iceberg.transforms.Transforms; import org.apache.iceberg.types.Types; import org.apache.iceberg.types.Types.NestedField; @@ -57,6 +59,56 @@ public class TestExpressionHelpers { private final UnboundPredicate pred = lessThan("x", 7); + @Test + public void testMillisLiteral() { + long now = System.currentTimeMillis(); + Literal millis = Expressions.millis(now); + assertThat(millis.value()).isEqualTo(now * 1000L); + assertThat(millis.to(Types.TimestampNanoType.withZone()).value()).isEqualTo(now * 1_000_000L); + } + + @Test + public void testMicrosLiteal() { + long ts = 1510842668000000L; + Literal micros = Expressions.micros(ts); + assertThat(micros.value()).isEqualTo(ts); + assertThat(micros.to(Types.TimestampNanoType.withZone()).value()).isEqualTo(ts * 1000L); + } + + @Test + public void testNanosLiteral() { + long ts = 1510842668000000001L; + Literal nanos = Expressions.nanos(ts); + assertThat(nanos.value()).isEqualTo(ts); + assertThat(nanos.to(Types.TimestampType.withoutZone()).value()).isEqualTo(1510842668000000L); + } + + @Test + public void testMixedTimestampLiterals() { + long now = System.currentTimeMillis(); + Schema schema = new Schema(NestedField.required(1, "ts", Types.TimestampType.withoutZone())); + + // all 3 timestamp values represent the same time and will be deduplicated + Expression expr = + Expressions.in( + "ts", + List.of( + Expressions.millis(now), + Expressions.micros(now * 1000), + Expressions.nanos(now * 1_000_000))); + + Expression bound = Binder.bind(schema.asStruct(), expr); + + // duplicates are replaced with a single value and in is converted to equals + assertThat(bound).isInstanceOf(BoundPredicate.class); + BoundPredicate predicate = (BoundPredicate) bound; + assertThat(predicate.isLiteralPredicate()).isTrue(); + assertThat(predicate.asLiteralPredicate().op()).isEqualTo(Expression.Operation.EQ); + assertThat(predicate.asLiteralPredicate().ref().type()) + .isEqualTo(Types.TimestampType.withoutZone()); + assertThat(predicate.asLiteralPredicate().literal().value()).isEqualTo(now * 1000); + } + @Test public void testSimplifyOr() { assertThat(or(alwaysTrue(), pred))