diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/spatial/Spatial.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/spatial/Spatial.adoc index 4bfe37412c..b2188dbc9f 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/spatial/Spatial.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/spatial/Spatial.adoc @@ -62,10 +62,9 @@ For Maven, you need to add the following dependency: ==== -Not all databases support all the functions defined by Hibernate Spatial. -The table below provides an overview of the functions provided by each database. If the function is defined in the -https://portal.opengeospatial.org/files/?artifact_id=829[Simple Feature Specification], the description references the -relevant section. +Hibernate defines common spatial functions uniformly over all databases. These +functions largely correspond to those specified in the https://portal.opengeospatial.org/files/?artifact_id=829[Simple Feature Specification]. Not all databases are capable of supporting every function, however. The table below details which functions are supported by various database systems. + :yes: icon:check[role="green"] :no: icon:times[role="red"] @@ -110,13 +109,31 @@ relevant section. |================================ ^(1)^ Argument Geometries need to have the same dimensionality. -[NOTE] -==== -In previous versions Hibernate Spatial registered the SFS spatial functions under names without the "st_" prefix. Starting -from Hibernate 6.0, the functions are registered both with and without the prefix. So, e.g., both `st_dimension(geom)` and -`dimension(geom)` will work. -==== +Note that beyond the common spatial functions mentioned above, Hibernate may define additional spatial functions for each database dialect. These will be documented in the +Database notes below. + === Database notes +[[spatial-configuration-dialect-postgresql]] +Postgresql:: + +The Postgresql dialect has support for the https://postgis.net/[Postgis spatial extension], but not the Geometric types mentioned in the +https://www.postgresql.org/docs/current/datatype-geometric.html[Postgresql documentation]. + +In addition tot he common spatial functions, the following functions are supported: + + +.Additional Postgis function support + +|=== +| Function | Purpose | Syntax | Postgis function operator +|`distance_2d` | 2D distance between two geometries|`distance_2d(geom,geom)`| https://postgis.net/docs/manual-3.3/geometry_distance_knn.html[\<\->] +|`distance_2d_bbox` | 2D distance between the bounding boxes of tow geometries|`distance_2d_bbox(geom,geom)`| https://postgis.net/docs/manual-3.3/geometry_distance_box.html[<#>] +|`distance_cpa` | 3D distance between 2 trajectories|`distance_cpa(geom,geom)`| https://postgis.net/docs/manual-3.3/geometry_distance_cpa.html[\|=\|] +|`distance_centroid_nd` | the n-D distance between the centroids of the bounding boxes of two geometries|`distance_centroid_nd(geom,geom)`| https://postgis.net/docs/manual-3.3/geometry_distance_centroid_nd.html[<\<\->>] + +|=== + + [[spatial-configuration-dialect-mysql]] MySQL:: @@ -202,10 +219,6 @@ create transform for db2gse.st_geometry db2_program ( [[spatial-types]] === Types -Hibernate Spatial comes with the following types: - -TODO - It suffices to declare a property as either a JTS or a Geolatte-geom `Geometry` and Hibernate Spatial will map it using the relevant type. diff --git a/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/postgis/PostgisSqmFunctionDescriptors.java b/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/postgis/PostgisSqmFunctionDescriptors.java index 31a3318e97..2412427b1a 100644 --- a/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/postgis/PostgisSqmFunctionDescriptors.java +++ b/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/postgis/PostgisSqmFunctionDescriptors.java @@ -7,22 +7,78 @@ package org.hibernate.spatial.dialect.postgis; -import java.util.Arrays; +import java.util.List; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; -import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.spatial.BaseSqmFunctionDescriptors; -import org.hibernate.spatial.CommonSpatialFunction; +import org.hibernate.spatial.FunctionKey; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.StandardBasicTypes; + +import static org.hibernate.query.sqm.produce.function.StandardArgumentsValidators.exactly; public class PostgisSqmFunctionDescriptors extends BaseSqmFunctionDescriptors { + private final BasicTypeRegistry typeRegistry; + public PostgisSqmFunctionDescriptors(FunctionContributions functionContributions) { super( functionContributions ); + typeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); + addOperator("distance_2d", "<->"); + addOperator("distance_2d_bbox", "<#>"); + addOperator("distance_cpa", "|=|"); + addOperator( "distance_centroid_nd", "<<->>" ); + // <<#>> operator is apparently no longer supported? + //addOperator( "distance_nd_bbox", "<<#>>" ); } + protected void addOperator(String name, String operator) { + map.put( + FunctionKey.apply( name ), + new PostgisOperator( + name, + operator, + exactly( 2 ), + StandardFunctionReturnTypeResolvers.invariant( typeRegistry.resolve( + StandardBasicTypes.DOUBLE ) + ) + ) + ); + } + static class PostgisOperator extends NamedSqmFunctionDescriptor { + final private String operator; + + public PostgisOperator( + String name, + String op, + ArgumentsValidator validator, + FunctionReturnTypeResolver returnTypeResolver) { + super( name, false, validator, returnTypeResolver ); + this.operator = op; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + sqlAppender.appendSql( '(' ); + final Expression arg1 = (Expression) sqlAstArguments.get( 0 ); + walker.render( arg1, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.appendSql( operator ); + final Expression arg2 = (Expression) sqlAstArguments.get( 1 ); + walker.render( arg2, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.appendSql( ')' ); + } + } } diff --git a/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDistanceOperatorsTest.java b/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDistanceOperatorsTest.java new file mode 100644 index 0000000000..412021d951 --- /dev/null +++ b/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDistanceOperatorsTest.java @@ -0,0 +1,144 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ + +package org.hibernate.spatial.dialect.postgis; + +import java.util.List; + +import org.hibernate.dialect.PostgreSQLDialect; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.TypedQuery; +import org.geolatte.geom.C2D; +import org.geolatte.geom.Point; +import org.geolatte.geom.crs.CoordinateReferenceSystem; +import org.geolatte.geom.crs.CoordinateReferenceSystems; + +import static org.geolatte.geom.builder.DSL.c; +import static org.geolatte.geom.builder.DSL.point; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the Postgis KNN distance functions (corresponding to the <-> and <<->> operators). + */ +@RequiresDialect(PostgreSQLDialect.class) +@DomainModel(annotatedClasses = { PostgisDistanceOperatorsTest.Neighbor.class }) +@SessionFactory(useCollectingStatementInspector = true) +public class PostgisDistanceOperatorsTest { + public static CoordinateReferenceSystem crs = CoordinateReferenceSystems.PROJECTED_2D_METER; + + private final Point searchPoint = point( crs, c( 0.0, 0.0 ) ); + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + for ( int i = 10; i > 0; i-- ) { + Neighbor neighbor = Neighbor.from( point( crs, c( i, 0.0 ) ), i ); + session.persist( neighbor ); + } + session.flush(); + session.clear(); + } + ); + } + + @Test + public void testDistance2D(SessionFactoryScope scope) { + SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery( + "select n from Neighbor n order by distance_2d(n.point, :pnt )", Neighbor.class ) + .setParameter( "pnt", searchPoint ); + List results = query.getResultList(); + assertFalse( results.isEmpty() ); + String sql = inspector.getSqlQueries().get( 0 ); + assertTrue(sql.matches(".*order by.*point\\w*<->.*"), "<-> operator is not rendered correctly"); + } + ); + } + + @Test + public void testDistance2DBBox(SessionFactoryScope scope) { + SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery( + "select n from Neighbor n order by distance_2d_bbox(n.point, :pnt )", Neighbor.class ) + .setParameter( "pnt", searchPoint ); + List results = query.getResultList(); + assertFalse( results.isEmpty() ); + String sql = inspector.getSqlQueries().get( 0 ); + assertTrue(sql.matches(".*order by.*point\\w*<#>.*"), "<#> operator is not rendered correctly"); + } + ); + } + + @Test + public void testDistanceNDBBox(SessionFactoryScope scope) { + SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery( + "select n from Neighbor n order by distance_centroid_nd(n.point, :pnt )", Neighbor.class ) + .setParameter( "pnt", searchPoint ); + List results = query.getResultList(); + assertFalse( results.isEmpty() ); + String sql = inspector.getSqlQueries().get( 0 ); + assertTrue(sql.matches(".*order by.*point\\w*<<->>.*"), "<<->>> operator is not rendered correctly"); + } + ); + } + + @AfterEach + public void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Neighbor" ); + } + ); + } + + @Entity(name = "Neighbor") + @Table(name = "neighbor") + public static class Neighbor { + + static Neighbor from(Point pnt, Integer i) { + Neighbor res = new Neighbor(); + res.point = pnt; + res.num = i; + return res; + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private Integer num; + + Point point; + } +} +