diff --git a/api/src/main/java/org/apache/iceberg/expressions/Expression.java b/api/src/main/java/org/apache/iceberg/expressions/Expression.java index 4a047e08099f..f2729686ab16 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Expression.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Expression.java @@ -44,6 +44,8 @@ enum Operation { OR, STARTS_WITH, NOT_STARTS_WITH, + ST_INTERSECTS, + ST_DISJOINT, COUNT, COUNT_NULL, COUNT_STAR, @@ -91,6 +93,10 @@ public Operation negate() { return Operation.NOT_STARTS_WITH; case NOT_STARTS_WITH: return Operation.STARTS_WITH; + case ST_INTERSECTS: + return Operation.ST_DISJOINT; + case ST_DISJOINT: + return Operation.ST_INTERSECTS; default: throw new IllegalArgumentException("No negation for operation: " + this); } diff --git a/api/src/main/java/org/apache/iceberg/expressions/ExpressionUtil.java b/api/src/main/java/org/apache/iceberg/expressions/ExpressionUtil.java index 9502e1d5bd6f..33b6fd006b1e 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/ExpressionUtil.java +++ b/api/src/main/java/org/apache/iceberg/expressions/ExpressionUtil.java @@ -33,6 +33,8 @@ import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; import org.apache.iceberg.Table; +import org.apache.iceberg.expressions.Literals.BoundingBoxLiteral; +import org.apache.iceberg.geospatial.BoundingBox; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; import org.apache.iceberg.relocated.com.google.common.collect.Lists; import org.apache.iceberg.transforms.Transforms; @@ -340,6 +342,8 @@ public Expression predicate(UnboundPredicate pred) { case NOT_EQ: case STARTS_WITH: case NOT_STARTS_WITH: + case ST_INTERSECTS: + case ST_DISJOINT: return new UnboundPredicate<>( pred.op(), pred.term(), (T) sanitize(pred.literal(), now, today)); case IN: @@ -440,6 +444,10 @@ public String predicate(BoundPredicate pred) { return term + " STARTS WITH " + value((BoundLiteralPredicate) pred); case NOT_STARTS_WITH: return term + " NOT STARTS WITH " + value((BoundLiteralPredicate) pred); + case ST_INTERSECTS: + return "st_intersects(" + term + ", " + value((BoundLiteralPredicate) pred) + ")"; + case ST_DISJOINT: + return "st_disjoint(" + term + ", " + value((BoundLiteralPredicate) pred) + ")"; default: throw new UnsupportedOperationException( "Cannot sanitize unsupported predicate type: " + pred.op()); @@ -492,6 +500,10 @@ public String predicate(UnboundPredicate pred) { return term + " STARTS WITH " + sanitize(pred.literal(), nowMicros, today); case NOT_STARTS_WITH: return term + " NOT STARTS WITH " + sanitize(pred.literal(), nowMicros, today); + case ST_INTERSECTS: + return "st_intersects(" + term + ", " + sanitize(pred.literal(), nowMicros, today) + ")"; + case ST_DISJOINT: + return "st_disjoint(" + term + ", " + sanitize(pred.literal(), nowMicros, today) + ")"; default: throw new UnsupportedOperationException( "Cannot sanitize unsupported predicate type: " + pred.op()); @@ -541,6 +553,8 @@ private static String sanitize(Literal literal, long now, int today) { return sanitizeNumber(((Literals.DoubleLiteral) literal).value(), "float"); } else if (literal instanceof Literals.VariantLiteral) { return sanitizeVariant(((Literals.VariantLiteral) literal).value(), now, today); + } else if (literal instanceof BoundingBoxLiteral) { + return sanitizeBoundingBox(((BoundingBoxLiteral) literal).value()); } else { // for uuid, decimal, fixed and binary, match the string result return sanitizeSimpleString(literal.value().toString()); @@ -619,6 +633,20 @@ private static String sanitizeSimpleString(CharSequence value) { return String.format(Locale.ROOT, "(hash-%08x)", HASH_FUNC.apply(value)); } + private static String sanitizeBoundingBox(BoundingBox bbox) { + boolean hasZ = bbox.min().hasZ() && bbox.max().hasZ(); + boolean hasM = bbox.min().hasM() && bbox.max().hasM(); + if (hasZ && hasM) { + return "(boundingbox-xyzm)"; + } else if (hasZ) { + return "(boundingbox-xyz)"; + } else if (hasM) { + return "(boundingbox-xym)"; + } else { + return "(boundingbox-xy)"; + } + } + private static String sanitizeVariant(Variant value, long now, int today) { return sanitizeVariant(value.value(), now, today); } diff --git a/api/src/main/java/org/apache/iceberg/expressions/ExpressionVisitors.java b/api/src/main/java/org/apache/iceberg/expressions/ExpressionVisitors.java index 79ca6a712887..b9f8b9421b4b 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/ExpressionVisitors.java +++ b/api/src/main/java/org/apache/iceberg/expressions/ExpressionVisitors.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.function.Supplier; import org.apache.iceberg.exceptions.ValidationException; +import org.apache.iceberg.geospatial.BoundingBox; /** Utils for traversing {@link Expression expressions}. */ public class ExpressionVisitors { @@ -126,6 +127,14 @@ public R notStartsWith(BoundReference ref, Literal lit) { "notStartsWith expression is not supported by the visitor"); } + public R stIntersects(BoundReference ref, BoundingBox bbox) { + throw new UnsupportedOperationException("stIntersects expression is not supported"); + } + + public R stDisjoint(BoundReference ref, BoundingBox bbox) { + throw new UnsupportedOperationException("stDisjoint expression is not supported"); + } + /** * Handle a non-reference value in this visitor. * @@ -141,6 +150,7 @@ public R handleNonReference(Bound term) { throw new ValidationException("Visitor %s does not support non-reference: %s", this, term); } + @SuppressWarnings("unchecked") @Override public R predicate(BoundPredicate pred) { if (!(pred.term() instanceof BoundReference)) { @@ -166,6 +176,14 @@ public R predicate(BoundPredicate pred) { return startsWith((BoundReference) pred.term(), literalPred.literal()); case NOT_STARTS_WITH: return notStartsWith((BoundReference) pred.term(), literalPred.literal()); + case ST_INTERSECTS: + return stIntersects( + (BoundReference) pred.term(), + ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); + case ST_DISJOINT: + return stDisjoint( + (BoundReference) pred.term(), + ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); default: throw new IllegalStateException( "Invalid operation for BoundLiteralPredicate: " + pred.op()); @@ -266,6 +284,16 @@ public R notStartsWith(Bound expr, Literal lit) { throw new UnsupportedOperationException("Unsupported operation."); } + public R stIntersects(Bound term, BoundingBox bbox) { + throw new UnsupportedOperationException( + "stIntersects operator is not supported by the visitor"); + } + + public R stDisjoint(Bound term, BoundingBox bbox) { + throw new UnsupportedOperationException( + "stDisjoint operator is not supported by the visitor"); + } + @Override public R predicate(BoundPredicate pred) { if (pred.isLiteralPredicate()) { @@ -287,6 +315,12 @@ public R predicate(BoundPredicate pred) { return startsWith(pred.term(), literalPred.literal()); case NOT_STARTS_WITH: return notStartsWith(pred.term(), literalPred.literal()); + case ST_INTERSECTS: + return stIntersects( + pred.term(), ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); + case ST_DISJOINT: + return stDisjoint( + pred.term(), ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); default: throw new IllegalStateException( "Invalid operation for BoundLiteralPredicate: " + pred.op()); @@ -465,6 +499,12 @@ public R predicate(BoundPredicate pred) { return startsWith(pred.term(), literalPred.literal()); case NOT_STARTS_WITH: return notStartsWith(pred.term(), literalPred.literal()); + case ST_INTERSECTS: + return stIntersects( + pred.term(), ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); + case ST_DISJOINT: + return stDisjoint( + pred.term(), ((Literals.BoundingBoxLiteral) literalPred.literal()).value()); default: throw new IllegalStateException( "Invalid operation for BoundLiteralPredicate: " + pred.op()); @@ -555,6 +595,14 @@ public R startsWith(BoundTerm term, Literal lit) { public R notStartsWith(BoundTerm term, Literal lit) { return null; } + + public R stIntersects(BoundTerm term, BoundingBox bbox) { + return null; + } + + public R stDisjoint(BoundTerm term, BoundingBox bbox) { + return null; + } } /** 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 78012def5a58..aab63ccc35d5 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Expressions.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Expressions.java @@ -18,8 +18,10 @@ */ package org.apache.iceberg.expressions; +import java.nio.ByteBuffer; import java.util.stream.Stream; import org.apache.iceberg.expressions.Expression.Operation; +import org.apache.iceberg.geospatial.BoundingBox; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.relocated.com.google.common.collect.Lists; import org.apache.iceberg.transforms.Transform; @@ -202,6 +204,14 @@ public static UnboundPredicate notStartsWith(UnboundTerm expr, S return new UnboundPredicate<>(Expression.Operation.NOT_STARTS_WITH, expr, value); } + public static UnboundPredicate stIntersects(String name, BoundingBox value) { + return geospatialPredicate(Operation.ST_INTERSECTS, name, value); + } + + public static UnboundPredicate stDisjoint(String name, BoundingBox value) { + return geospatialPredicate(Operation.ST_DISJOINT, name, value); + } + public static UnboundPredicate in(String name, T... values) { return predicate(Operation.IN, name, Lists.newArrayList(values)); } @@ -280,6 +290,13 @@ public static UnboundPredicate predicate(Operation op, UnboundTerm exp return new UnboundPredicate<>(op, expr); } + @SuppressWarnings("unchecked") + static UnboundPredicate geospatialPredicate( + Operation op, String name, BoundingBox value) { + return new UnboundPredicate( + op, ref(name), (Literal) (Literal) Literal.of(value)); + } + public static True alwaysTrue() { return True.INSTANCE; } diff --git a/api/src/main/java/org/apache/iceberg/expressions/Literal.java b/api/src/main/java/org/apache/iceberg/expressions/Literal.java index b5d6f72f74d0..d4e2c6399c61 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Literal.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Literal.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import java.util.Comparator; import java.util.UUID; +import org.apache.iceberg.geospatial.BoundingBox; import org.apache.iceberg.types.Type; /** @@ -71,6 +72,10 @@ static Literal of(BigDecimal value) { return new Literals.DecimalLiteral(value); } + static Literal of(BoundingBox value) { + return new Literals.BoundingBoxLiteral(value); + } + /** Returns the value wrapped by this literal. */ T value(); 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 c54dff72f87e..b0dc2fe627c3 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/Literals.java +++ b/api/src/main/java/org/apache/iceberg/expressions/Literals.java @@ -33,6 +33,8 @@ import java.util.Locale; import java.util.Objects; import java.util.UUID; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.relocated.com.google.common.io.BaseEncoding; import org.apache.iceberg.types.Comparators; @@ -87,6 +89,8 @@ static Literal from(T value) { return (Literal) new Literals.DecimalLiteral((BigDecimal) value); } else if (value instanceof Variant) { return (Literal) new Literals.VariantLiteral((Variant) value); + } else if (value instanceof BoundingBox) { + return (Literal) new Literals.BoundingBoxLiteral((BoundingBox) value); } throw new IllegalArgumentException( @@ -747,4 +751,96 @@ public String toString() { return "X'" + BASE16_ENCODING.encode(bytes) + "'"; } } + + static class BoundingBoxLiteral implements Literal { + private static final Comparator CMP = + Comparators.nullsFirst().thenComparing(BoundingBoxLiteral::compare); + + private final BoundingBox value; + + BoundingBoxLiteral(BoundingBox value) { + Preconditions.checkNotNull(value, "Bounding box value cannot be null"); + this.value = value; + } + + @Override + public BoundingBox value() { + return value; + } + + @Override + public ByteBuffer toByteBuffer() { + return value.toByteBuffer(); + } + + @Override + @SuppressWarnings("unchecked") + public Literal to(Type type) { + if (type.typeId() != Type.TypeID.GEOMETRY && type.typeId() != Type.TypeID.GEOGRAPHY) { + return null; + } + + return (Literal) this; + } + + @Override + public Comparator comparator() { + return CMP; + } + + Object writeReplace() throws ObjectStreamException { + return new SerializationProxies.BoundingBoxLiteralProxy(toByteBuffer()); + } + + @Override + public String toString() { + return value().toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BoundingBoxLiteral)) { + return false; + } + + BoundingBoxLiteral that = (BoundingBoxLiteral) other; + return comparator().compare(value(), that.value()) == 0; + } + + @Override + public int hashCode() { + return Objects.hashCode(value()); + } + + private static int compare(BoundingBox left, BoundingBox right) { + int cmp = compare(left.min(), right.min()); + if (cmp != 0) { + return cmp; + } + + return compare(left.max(), right.max()); + } + + private static int compare(GeospatialBound left, GeospatialBound right) { + int cmp = Double.compare(left.x(), right.x()); + if (cmp != 0) { + return cmp; + } + + cmp = Double.compare(left.y(), right.y()); + if (cmp != 0) { + return cmp; + } + + cmp = Double.compare(left.z(), right.z()); + if (cmp != 0) { + return cmp; + } + + return Double.compare(left.m(), right.m()); + } + } } diff --git a/api/src/main/java/org/apache/iceberg/expressions/SerializationProxies.java b/api/src/main/java/org/apache/iceberg/expressions/SerializationProxies.java index 59fd231cd368..24bea2437be6 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/SerializationProxies.java +++ b/api/src/main/java/org/apache/iceberg/expressions/SerializationProxies.java @@ -21,6 +21,7 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.nio.ByteBuffer; +import org.apache.iceberg.geospatial.BoundingBox; /** * Stand-in classes for expression classes in Java Serialization. @@ -81,4 +82,20 @@ protected byte[] bytes() { return bytes; } } + + static class BoundingBoxLiteralProxy implements Serializable { + private byte[] bytes; + + /** Constructor for Java serialization. */ + BoundingBoxLiteralProxy() {} + + BoundingBoxLiteralProxy(ByteBuffer buffer) { + this.bytes = new byte[buffer.remaining()]; + buffer.duplicate().get(this.bytes); + } + + Object readResolve() throws ObjectStreamException { + return new Literals.BoundingBoxLiteral(BoundingBox.fromByteBuffer(ByteBuffer.wrap(bytes))); + } + } } diff --git a/api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java b/api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java index 75ca9d5835bc..76b3c97a4ed6 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java +++ b/api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java @@ -284,6 +284,10 @@ public String toString() { return term() + " startsWith \"" + literal() + "\""; case NOT_STARTS_WITH: return term() + " notStartsWith \"" + literal() + "\""; + case ST_INTERSECTS: + return "st_intersects(" + term() + ", " + literal() + ")"; + case ST_DISJOINT: + return "st_disjoint(" + term() + ", " + literal() + ")"; case IN: return term() + " in (" + COMMA.join(literals()) + ")"; case NOT_IN: diff --git a/api/src/main/java/org/apache/iceberg/geospatial/BoundingBox.java b/api/src/main/java/org/apache/iceberg/geospatial/BoundingBox.java index cbf3c699ce1f..c656e117ba0d 100644 --- a/api/src/main/java/org/apache/iceberg/geospatial/BoundingBox.java +++ b/api/src/main/java/org/apache/iceberg/geospatial/BoundingBox.java @@ -52,8 +52,6 @@ public static BoundingBox fromByteBuffers(ByteBuffer min, ByteBuffer max) { */ public static BoundingBox fromByteBuffer(ByteBuffer buffer) { Preconditions.checkArgument(buffer.position() == 0, "Input ByteBuffer must have position 0"); - Preconditions.checkArgument( - buffer.order() == ByteOrder.LITTLE_ENDIAN, "Invalid byte order: big endian"); ByteBuffer tmp = buffer.duplicate(); tmp.order(ByteOrder.LITTLE_ENDIAN); diff --git a/api/src/main/java/org/apache/iceberg/geospatial/GeospatialBound.java b/api/src/main/java/org/apache/iceberg/geospatial/GeospatialBound.java index 3425a4663801..b128ba0ac9da 100644 --- a/api/src/main/java/org/apache/iceberg/geospatial/GeospatialBound.java +++ b/api/src/main/java/org/apache/iceberg/geospatial/GeospatialBound.java @@ -63,8 +63,6 @@ public class GeospatialBound { * @throws IllegalArgumentException if the buffer has an invalid size */ public static GeospatialBound fromByteBuffer(ByteBuffer buffer) { - Preconditions.checkArgument( - buffer.order() == ByteOrder.LITTLE_ENDIAN, "Invalid byte order: big endian"); ByteBuffer tmp = buffer.duplicate(); tmp.order(ByteOrder.LITTLE_ENDIAN); int size = tmp.remaining(); diff --git a/api/src/main/java/org/apache/iceberg/types/Conversions.java b/api/src/main/java/org/apache/iceberg/types/Conversions.java index 5966c894be5f..279d5dfdc079 100644 --- a/api/src/main/java/org/apache/iceberg/types/Conversions.java +++ b/api/src/main/java/org/apache/iceberg/types/Conversions.java @@ -120,6 +120,11 @@ public static ByteBuffer toByteBuffer(Type.TypeID typeId, Object value) { return (ByteBuffer) value; case DECIMAL: return ByteBuffer.wrap(((BigDecimal) value).unscaledValue().toByteArray()); + case GEOMETRY: + case GEOGRAPHY: + // GEOMETRY and GEOGRAPHY values are represented as byte buffers in WKB + // (Well-Known Binary) format in Iceberg. Return the byte buffer as is. + return (ByteBuffer) value; case VARIANT: // Produce a concatenated buffer of metadata and value Variant variant = (Variant) value; @@ -194,6 +199,11 @@ private static Object internalFromByteBuffer(Type type, ByteBuffer buffer) { byte[] unscaledBytes = new byte[buffer.remaining()]; tmp.get(unscaledBytes); return new BigDecimal(new BigInteger(unscaledBytes), decimal.scale()); + case GEOMETRY: + case GEOGRAPHY: + // GEOMETRY and GEOGRAPHY values are represented as byte buffers in WKB + // (Well-Known Binary) format in Iceberg. Return the byte buffer as is. + return tmp; case VARIANT: return Variant.from(tmp); case UNKNOWN: diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionBinding.java b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionBinding.java index 24e58ad1e808..152247d689e3 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionBinding.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionBinding.java @@ -44,6 +44,8 @@ import org.apache.iceberg.Schema; import org.apache.iceberg.TestHelpers; import org.apache.iceberg.exceptions.ValidationException; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.types.Types; import org.apache.iceberg.types.Types.StructType; @@ -62,7 +64,9 @@ public class TestExpressionBinding { required(3, "data", Types.StringType.get()), required(4, "var", Types.VariantType.get()), optional(5, "nullable", Types.IntegerType.get()), - optional(6, "always_null", Types.UnknownType.get())); + optional(6, "always_null", Types.UnknownType.get()), + required(7, "geometry", Types.GeometryType.crs84()), + required(8, "geography", Types.GeographyType.crs84())); @Test public void testMissingReference() { @@ -509,4 +513,48 @@ public void testIsNullWithNestedStructs(List requiredFields, Expression TestHelpers.assertAllReferencesBound("NotNull", bound); assertThat(bound.op()).isEqualTo(expression.negate().op()); } + + private static BoundingBox testBoundingBox() { + GeospatialBound min = GeospatialBound.createXY(1.0, 2.0); + GeospatialBound max = GeospatialBound.createXY(3.0, 4.0); + return new BoundingBox(min, max); + } + + private static Stream spatialPredicateCases() { + return Stream.of(Arguments.of("geometry", 7), Arguments.of("geography", 8)); + } + + @ParameterizedTest + @MethodSource("spatialPredicateCases") + public void testStIntersectsBinding(String fieldName, int fieldId) { + BoundingBox bbox = testBoundingBox(); + + Expression expr = Expressions.stIntersects(fieldName, bbox); + Expression bound = Binder.bind(STRUCT, expr); + + TestHelpers.assertAllReferencesBound("ST_Intersects", bound); + BoundPredicate pred = TestHelpers.assertAndUnwrap(bound); + assertThat(pred.op()).isEqualTo(Expression.Operation.ST_INTERSECTS); + assertThat(pred.term().ref().fieldId()) + .as("Should bind %s correctly", fieldName) + .isEqualTo(fieldId); + assertThat(pred.asLiteralPredicate().literal().value()).isEqualTo(bbox); + } + + @ParameterizedTest + @MethodSource("spatialPredicateCases") + public void testStDisjointBinding(String fieldName, int fieldId) { + BoundingBox bbox = testBoundingBox(); + + Expression expr = Expressions.stDisjoint(fieldName, bbox); + Expression bound = Binder.bind(STRUCT, expr); + + TestHelpers.assertAllReferencesBound("ST_Disjoint", bound); + BoundPredicate pred = TestHelpers.assertAndUnwrap(bound); + assertThat(pred.op()).isEqualTo(Expression.Operation.ST_DISJOINT); + assertThat(pred.term().ref().fieldId()) + .as("Should bind %s correctly", fieldName) + .isEqualTo(fieldId); + assertThat(pred.asLiteralPredicate().literal().value()).isEqualTo(bbox); + } } 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 2a1fab10a445..c4d1efc7eaa2 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionHelpers.java @@ -41,6 +41,8 @@ import static org.apache.iceberg.expressions.Expressions.predicate; import static org.apache.iceberg.expressions.Expressions.ref; import static org.apache.iceberg.expressions.Expressions.rewriteNot; +import static org.apache.iceberg.expressions.Expressions.stDisjoint; +import static org.apache.iceberg.expressions.Expressions.stIntersects; import static org.apache.iceberg.expressions.Expressions.startsWith; import static org.apache.iceberg.expressions.Expressions.truncate; import static org.apache.iceberg.expressions.Expressions.year; @@ -50,6 +52,8 @@ import java.util.List; import java.util.concurrent.Callable; import org.apache.iceberg.Schema; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.transforms.Transforms; import org.apache.iceberg.types.Types; import org.apache.iceberg.types.Types.NestedField; @@ -148,7 +152,11 @@ public void testRewriteNot() { StructType struct = StructType.of( NestedField.optional(1, "a", Types.IntegerType.get()), - NestedField.optional(2, "s", Types.StringType.get())); + NestedField.optional(2, "s", Types.StringType.get()), + NestedField.optional(3, "geometry", Types.GeometryType.crs84()), + NestedField.optional(4, "geography", Types.GeographyType.crs84())); + BoundingBox bbox = + new BoundingBox(GeospatialBound.createXY(10, 20), GeospatialBound.createXY(30, 40)); Expression[][] expressions = new Expression[][] { // (rewritten pred, original pred) pairs @@ -178,7 +186,11 @@ public void testRewriteNot() { {or(equal("a", 5), isNull("a")), not(and(notEqual("a", 5), notNull("a")))}, {or(equal("a", 5), notNull("a")), or(equal("a", 5), not(isNull("a")))}, {startsWith("s", "hello"), not(notStartsWith("s", "hello"))}, - {notStartsWith("s", "world"), not(startsWith("s", "world"))} + {notStartsWith("s", "world"), not(startsWith("s", "world"))}, + {stIntersects("geometry", bbox), not(stDisjoint("geometry", bbox))}, + {stDisjoint("geometry", bbox), not(stIntersects("geometry", bbox))}, + {stIntersects("geography", bbox), not(stDisjoint("geography", bbox))}, + {stDisjoint("geography", bbox), not(stIntersects("geography", bbox))}, }; for (Expression[] pair : expressions) { diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionSerialization.java b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionSerialization.java index fc7ddd035bf2..a83a86b70132 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionSerialization.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionSerialization.java @@ -24,6 +24,8 @@ import org.apache.iceberg.Schema; import org.apache.iceberg.TestHelpers; import org.apache.iceberg.expressions.Expression.Operation; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.types.Types; import org.junit.jupiter.api.Test; @@ -33,7 +35,9 @@ public void testExpressions() throws Exception { Schema schema = new Schema( Types.NestedField.optional(34, "a", Types.IntegerType.get()), - Types.NestedField.required(35, "s", Types.StringType.get())); + Types.NestedField.required(35, "s", Types.StringType.get()), + Types.NestedField.required(36, "geometry", Types.GeometryType.crs84()), + Types.NestedField.required(37, "geography", Types.GeographyType.crs84())); Expression[] expressions = new Expression[] { @@ -61,7 +65,26 @@ public void testExpressions() throws Exception { Expressions.notIn("s", "abc", "xyz").bind(schema.asStruct()), Expressions.isNull("a").bind(schema.asStruct()), Expressions.startsWith("s", "abc").bind(schema.asStruct()), - Expressions.notStartsWith("s", "xyz").bind(schema.asStruct()) + Expressions.notStartsWith("s", "xyz").bind(schema.asStruct()), + Expressions.notStartsWith("s", "xyz").bind(schema.asStruct()), + Expressions.stIntersects( + "geometry", + new BoundingBox( + GeospatialBound.createXY(1.0, 2.0), GeospatialBound.createXY(3.0, 4.0))), + Expressions.stDisjoint( + "geography", + new BoundingBox( + GeospatialBound.createXY(5.0, 6.0), GeospatialBound.createXY(7.0, 8.0))), + Expressions.stIntersects( + "geometry", + new BoundingBox( + GeospatialBound.createXY(1.0, 2.0), GeospatialBound.createXY(3.0, 4.0))) + .bind(schema.asStruct()), + Expressions.stDisjoint( + "geography", + new BoundingBox( + GeospatialBound.createXY(5.0, 6.0), GeospatialBound.createXY(7.0, 8.0))) + .bind(schema.asStruct()) }; for (Expression expression : expressions) { diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionUtil.java b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionUtil.java index a5281421888f..4d0e5ebaabe7 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestExpressionUtil.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestExpressionUtil.java @@ -33,9 +33,12 @@ import java.util.Map; import java.util.regex.Pattern; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; import org.apache.iceberg.Table; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.relocated.com.google.common.collect.Lists; import org.apache.iceberg.types.Types; @@ -48,6 +51,9 @@ import org.apache.iceberg.variants.VariantTestUtil; import org.apache.iceberg.variants.VariantValue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class TestExpressionUtil { private static final Schema SCHEMA = @@ -62,7 +68,9 @@ public class TestExpressionUtil { Types.NestedField.optional(8, "data", Types.StringType.get()), Types.NestedField.optional(9, "measurement", Types.DoubleType.get()), Types.NestedField.optional(10, "test", Types.IntegerType.get()), - Types.NestedField.required(11, "var", Types.VariantType.get())); + Types.NestedField.required(11, "var", Types.VariantType.get()), + Types.NestedField.required(12, "geometry", Types.GeometryType.crs84()), + Types.NestedField.required(13, "geography", Types.GeographyType.crs84())); private static final Types.StructType STRUCT = SCHEMA.asStruct(); @@ -1334,6 +1342,76 @@ private static VariantArray createArrayWithNestedTypes() { return (VariantArray) VariantValue.from(metadata, variantBB); } + private static Stream geospatialPredicateParameters() { + GeospatialBound min = GeospatialBound.createXY(1.0, 2.0); + GeospatialBound max = GeospatialBound.createXY(3.0, 4.0); + BoundingBox bbox = new BoundingBox(min, max); + + GeospatialBound minXYZ = GeospatialBound.createXYZ(1.0, 2.0, 3.0); + GeospatialBound maxXYZ = GeospatialBound.createXYZ(3.0, 4.0, 5.0); + BoundingBox bboxXYZ = new BoundingBox(minXYZ, maxXYZ); + + GeospatialBound minXYM = GeospatialBound.createXYM(1.0, 2.0, 3.0); + GeospatialBound maxXYM = GeospatialBound.createXYM(3.0, 4.0, 5.0); + BoundingBox bboxXYM = new BoundingBox(minXYM, maxXYM); + + GeospatialBound minXYZM = GeospatialBound.createXYZM(1.0, 2.0, 3.0, 4.0); + GeospatialBound maxXYZM = GeospatialBound.createXYZM(3.0, 4.0, 5.0, 6.0); + BoundingBox bboxXYZM = new BoundingBox(minXYZM, maxXYZM); + + return Stream.of( + Arguments.of( + Expressions.stIntersects("geometry", bbox), + "st_intersects(geometry, (boundingbox-xy))", + "(boundingbox-xy)"), + Arguments.of( + Expressions.stIntersects("geography", bbox), + "st_intersects(geography, (boundingbox-xy))", + "(boundingbox-xy)"), + Arguments.of( + Expressions.stDisjoint("geometry", bbox), + "st_disjoint(geometry, (boundingbox-xy))", + "(boundingbox-xy)"), + Arguments.of( + Expressions.stDisjoint("geography", bbox), + "st_disjoint(geography, (boundingbox-xy))", + "(boundingbox-xy)"), + Arguments.of( + Expressions.stIntersects("geometry", bboxXYZ), + "st_intersects(geometry, (boundingbox-xyz))", + "(boundingbox-xyz)"), + Arguments.of( + Expressions.stIntersects("geometry", bboxXYM), + "st_intersects(geometry, (boundingbox-xym))", + "(boundingbox-xym)"), + Arguments.of( + Expressions.stIntersects("geometry", bboxXYZM), + "st_intersects(geometry, (boundingbox-xyzm))", + "(boundingbox-xyzm)")); + } + + @ParameterizedTest + @MethodSource("geospatialPredicateParameters") + public void testSanitizeGeospatialPredicates( + UnboundPredicate geoPredicate, + String expectedSanitizedString, + String expectedLiteral) { + Expression.Operation operation = geoPredicate.op(); + String columnName = geoPredicate.term().ref().name(); + + Expression predicateSanitized = Expressions.predicate(operation, columnName, expectedLiteral); + assertEquals(predicateSanitized, ExpressionUtil.sanitize(geoPredicate)); + assertEquals(predicateSanitized, ExpressionUtil.sanitize(STRUCT, geoPredicate, true)); + + assertThat(ExpressionUtil.toSanitizedString(geoPredicate)) + .as("Sanitized string should be identical for geospatial predicates") + .isEqualTo(expectedSanitizedString); + + assertThat(ExpressionUtil.toSanitizedString(STRUCT, geoPredicate, true)) + .as("Sanitized string should be identical for geospatial predicates") + .isEqualTo(expectedSanitizedString); + } + private void assertEquals(Expression expected, Expression actual) { assertThat(expected).isInstanceOf(UnboundPredicate.class); assertEquals((UnboundPredicate) expected, (UnboundPredicate) actual); diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestLiteralSerialization.java b/api/src/test/java/org/apache/iceberg/expressions/TestLiteralSerialization.java index 24fc458b37b4..d82032b258b6 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestLiteralSerialization.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestLiteralSerialization.java @@ -23,6 +23,8 @@ import java.math.BigDecimal; import java.util.UUID; import org.apache.iceberg.TestHelpers; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.types.Types; import org.junit.jupiter.api.Test; @@ -47,6 +49,9 @@ public void testLiterals() throws Exception { Literal.of(new byte[] {1, 2, 3}).to(Types.FixedType.ofLength(3)), Literal.of(new byte[] {3, 4, 5, 6}).to(Types.BinaryType.get()), Literal.of(new BigDecimal("122.50")), + Literal.of( + new BoundingBox( + GeospatialBound.createXY(1.0, 2.0), GeospatialBound.createXY(3.0, 4.0))) }; for (Literal lit : literals) { diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestMiscLiteralConversions.java b/api/src/test/java/org/apache/iceberg/expressions/TestMiscLiteralConversions.java index 53aec44ac7ce..cb75e1a5fa7b 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestMiscLiteralConversions.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestMiscLiteralConversions.java @@ -25,6 +25,8 @@ import java.util.Arrays; import java.util.List; import java.util.UUID; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.types.Type; import org.apache.iceberg.types.Types; import org.junit.jupiter.api.Test; @@ -48,7 +50,17 @@ public void testIdentityConversions() { Pair.of(Literal.of("abc"), Types.StringType.get()), Pair.of(Literal.of(UUID.randomUUID()), Types.UUIDType.get()), Pair.of(Literal.of(new byte[] {0, 1, 2}), Types.FixedType.ofLength(3)), - Pair.of(Literal.of(ByteBuffer.wrap(new byte[] {0, 1, 2})), Types.BinaryType.get())); + Pair.of(Literal.of(ByteBuffer.wrap(new byte[] {0, 1, 2})), Types.BinaryType.get()), + Pair.of( + Literal.of( + new BoundingBox( + GeospatialBound.createXY(0, 1), GeospatialBound.createXY(2, 3))), + Types.GeometryType.crs84()), + Pair.of( + Literal.of( + new BoundingBox( + GeospatialBound.createXY(0, 1), GeospatialBound.createXY(2, 3))), + Types.GeographyType.crs84())); for (Pair, Type> pair : pairs) { Literal lit = pair.first(); @@ -181,7 +193,9 @@ public void testInvalidBooleanConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -197,7 +211,9 @@ public void testInvalidIntegerConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -208,7 +224,9 @@ public void testInvalidLongConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -227,7 +245,9 @@ public void testInvalidFloatConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -246,7 +266,9 @@ public void testInvalidDoubleConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -267,7 +289,9 @@ public void testInvalidDateConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -288,7 +312,9 @@ public void testInvalidTimeConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -305,7 +331,9 @@ public void testInvalidTimestampMicrosConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -322,7 +350,9 @@ public void testInvalidTimestampNanosConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -343,7 +373,9 @@ public void testInvalidDecimalConversions() { Types.StringType.get(), Types.UUIDType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -358,7 +390,9 @@ public void testInvalidStringConversions() { Types.FloatType.get(), Types.DoubleType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -379,7 +413,9 @@ public void testInvalidUUIDConversions() { Types.DecimalType.of(9, 2), Types.StringType.get(), Types.FixedType.ofLength(1), - Types.BinaryType.get()); + Types.BinaryType.get(), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -400,7 +436,9 @@ public void testInvalidFixedConversions() { Types.DecimalType.of(9, 2), Types.StringType.get(), Types.UUIDType.get(), - Types.FixedType.ofLength(1)); + Types.FixedType.ofLength(1), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); } @Test @@ -421,9 +459,77 @@ public void testInvalidBinaryConversions() { Types.DecimalType.of(9, 2), Types.StringType.get(), Types.UUIDType.get(), + Types.FixedType.ofLength(1), + Types.GeometryType.crs84(), + Types.GeographyType.crs84()); + } + + @Test + public void testInvalidGeospatialConversions() { + GeospatialBound min = GeospatialBound.createXY(1.0, 2.0); + GeospatialBound max = GeospatialBound.createXY(3.0, 4.0); + Literal geoBoundingBoxLiteral = Literal.of(new BoundingBox(min, max)); + testInvalidConversions( + geoBoundingBoxLiteral, + Types.BooleanType.get(), + Types.IntegerType.get(), + Types.LongType.get(), + Types.FloatType.get(), + Types.DoubleType.get(), + Types.DateType.get(), + Types.TimeType.get(), + Types.DecimalType.of(9, 2), + Types.StringType.get(), + Types.UUIDType.get(), + Types.BinaryType.get(), Types.FixedType.ofLength(1)); } + @Test + public void testBoundingBoxLiteralComparator() { + Literal xy = + Literal.of( + new BoundingBox( + GeospatialBound.createXY(1.0, 2.0), GeospatialBound.createXY(3.0, 4.0))); + Literal sameMinLargerMax = + Literal.of( + new BoundingBox( + GeospatialBound.createXY(1.0, 2.0), GeospatialBound.createXY(4.0, 5.0))); + Literal largerMin = + Literal.of( + new BoundingBox( + GeospatialBound.createXY(2.0, 2.0), GeospatialBound.createXY(3.0, 4.0))); + Literal xyz = + Literal.of( + new BoundingBox( + GeospatialBound.createXYZ(1.0, 2.0, 3.0), + GeospatialBound.createXYZ(3.0, 4.0, 5.0))); + Literal xym = + Literal.of( + new BoundingBox( + GeospatialBound.createXYM(1.0, 2.0, 3.0), + GeospatialBound.createXYM(3.0, 4.0, 5.0))); + Literal xyzm = + Literal.of( + new BoundingBox( + GeospatialBound.createXYZM(1.0, 2.0, 3.0, 4.0), + GeospatialBound.createXYZM(3.0, 4.0, 5.0, 6.0))); + + assertThat(xy.comparator().compare(xy.value(), xy.value())).isZero(); + assertThat(xym.comparator().compare(xym.value(), xym.value())).isZero(); + assertThat(xyz.comparator().compare(xyz.value(), xyz.value())).isZero(); + assertThat(xyzm.comparator().compare(xyzm.value(), xyzm.value())).isZero(); + + assertThat(xy.comparator().compare(xy.value(), sameMinLargerMax.value())).isLessThan(0); + assertThat(xy.comparator().compare(xy.value(), largerMin.value())).isLessThan(0); + assertThat(xy.comparator().compare(xy.value(), xyz.value())).isNotZero(); + assertThat(xy.comparator().compare(xy.value(), xym.value())).isNotZero(); + assertThat(xy.comparator().compare(xy.value(), xyzm.value())).isNotZero(); + assertThat(xym.comparator().compare(xyz.value(), xym.value())).isNotZero(); + assertThat(xyz.comparator().compare(xyz.value(), xyzm.value())).isNotZero(); + assertThat(xym.comparator().compare(xym.value(), xyzm.value())).isNotZero(); + } + private void testInvalidConversions(Literal lit, Type... invalidTypes) { for (Type type : invalidTypes) { assertThat(lit.to(type)) diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestPredicateBinding.java b/api/src/test/java/org/apache/iceberg/expressions/TestPredicateBinding.java index a07c8fd1569d..57a0dc29089d 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestPredicateBinding.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestPredicateBinding.java @@ -41,10 +41,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.math.BigDecimal; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.apache.iceberg.exceptions.ValidationException; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.types.Types; import org.apache.iceberg.types.Types.StructType; import org.junit.jupiter.api.Test; @@ -648,4 +651,49 @@ public void testNotInPredicateBindingConversionToExpression() { .as("Should change NOT_IN to alwaysTrue expression") .isEqualTo(Expressions.alwaysTrue()); } + + @Test + public void testGeospatialPredicateBinding() { + StructType struct = + StructType.of( + required(20, "geometry", Types.GeometryType.crs84()), + required(21, "geography", Types.GeographyType.crs84())); + + // Create a bounding box for testing + GeospatialBound min = GeospatialBound.createXY(1.0, 2.0); + GeospatialBound max = GeospatialBound.createXY(3.0, 4.0); + BoundingBox bbox = new BoundingBox(min, max); + + // Test ST_INTERSECTS with geometry + UnboundPredicate stIntersectsGeom = Expressions.stIntersects("geometry", bbox); + Expression expr = stIntersectsGeom.bind(struct); + BoundPredicate bound = assertAndUnwrap(expr); + assertThat(bound.op()).isEqualTo(Expression.Operation.ST_INTERSECTS); + assertThat(bound.term().ref().fieldId()).isEqualTo(20); + assertThat(bound.asLiteralPredicate().literal().value()).isEqualTo(bbox); + + // Test ST_DISJOINT with geometry + UnboundPredicate stDisjointGeom = Expressions.stDisjoint("geometry", bbox); + expr = stDisjointGeom.bind(struct); + bound = assertAndUnwrap(expr); + assertThat(bound.op()).isEqualTo(Expression.Operation.ST_DISJOINT); + assertThat(bound.term().ref().fieldId()).isEqualTo(20); + assertThat(bound.asLiteralPredicate().literal().value()).isEqualTo(bbox); + + // Test ST_INTERSECTS with geography + UnboundPredicate stIntersectsGeog = Expressions.stIntersects("geography", bbox); + expr = stIntersectsGeog.bind(struct); + bound = assertAndUnwrap(expr); + assertThat(bound.op()).isEqualTo(Expression.Operation.ST_INTERSECTS); + assertThat(bound.term().ref().fieldId()).isEqualTo(21); + assertThat(bound.asLiteralPredicate().literal().value()).isEqualTo(bbox); + + // Test ST_DISJOINT with geography + UnboundPredicate stDisjointGeog = Expressions.stDisjoint("geography", bbox); + expr = stDisjointGeog.bind(struct); + bound = assertAndUnwrap(expr); + assertThat(bound.op()).isEqualTo(Expression.Operation.ST_DISJOINT); + assertThat(bound.term().ref().fieldId()).isEqualTo(21); + assertThat(bound.asLiteralPredicate().literal().value()).isEqualTo(bbox); + } } diff --git a/api/src/test/java/org/apache/iceberg/types/TestConversions.java b/api/src/test/java/org/apache/iceberg/types/TestConversions.java index e207cfd8d59a..aea5c0a0cfa9 100644 --- a/api/src/test/java/org/apache/iceberg/types/TestConversions.java +++ b/api/src/test/java/org/apache/iceberg/types/TestConversions.java @@ -26,6 +26,8 @@ import java.nio.charset.StandardCharsets; import java.util.UUID; import org.apache.iceberg.expressions.Literal; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; import org.apache.iceberg.types.Types.BinaryType; import org.apache.iceberg.types.Types.BooleanType; import org.apache.iceberg.types.Types.DateType; @@ -191,6 +193,19 @@ public void testByteBufferConversions() { .isEqualTo(new byte[] {11}); } + @Test + public void testGeospatialByteBufferConversions() { + // geospatial values were kept as-is + // this is the WKB representation of POINT (2 3) + byte[] wkb = new byte[] {1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 8, 64}; + assertConversion(ByteBuffer.wrap(wkb), Types.GeometryType.crs84(), wkb); + assertConversion(ByteBuffer.wrap(wkb), Types.GeographyType.crs84(), wkb); + // geospatial bounding boxes are converted to their byte buffer representation + BoundingBox bbox = + new BoundingBox(GeospatialBound.createXY(10, 20), GeospatialBound.createXY(30, 40)); + assertThat(Literal.of(bbox).toByteBuffer().array()).isEqualTo(bbox.toByteBuffer().array()); + } + private void assertConversion(T value, Type type, byte[] expectedBinary) { ByteBuffer byteBuffer = Conversions.toByteBuffer(type, value); assertThat(byteBuffer.array()).isEqualTo(expectedBinary);