HHH-15160 - Adds the Postgis distance operators

This commit is contained in:
Karel Maesen 2023-04-26 21:43:29 +02:00 committed by Christian Beikov
parent 6746c30275
commit 2a1aa73319
3 changed files with 230 additions and 17 deletions

View File

@ -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.

View File

@ -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<? extends SqlAstNode> 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( ')' );
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<C2D> crs = CoordinateReferenceSystems.PROJECTED_2D_METER;
private final Point<C2D> 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<Neighbor> query = session.createQuery(
"select n from Neighbor n order by distance_2d(n.point, :pnt )", Neighbor.class )
.setParameter( "pnt", searchPoint );
List<Neighbor> 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<Neighbor> query = session.createQuery(
"select n from Neighbor n order by distance_2d_bbox(n.point, :pnt )", Neighbor.class )
.setParameter( "pnt", searchPoint );
List<Neighbor> 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<Neighbor> query = session.createQuery(
"select n from Neighbor n order by distance_centroid_nd(n.point, :pnt )", Neighbor.class )
.setParameter( "pnt", searchPoint );
List<Neighbor> 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<C2D> 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<C2D> point;
}
}