From 08edf5e16c61f4b83c48b5ce407a5d1245d1a1a7 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 4 Jun 2022 19:06:51 +0200 Subject: [PATCH] Translate spatial aggregates Closes #2384 --- ...opologySuiteServiceCollectionExtensions.cs | 1 + ...uiteAggregateMethodCallTranslatorPlugin.cs | 94 +++++++++++++++++++ ...TopologySuiteMethodCallTranslatorPlugin.cs | 3 +- .../Query/SpatialQueryNpgsqlGeographyTest.cs | 5 +- .../Query/SpatialQueryNpgsqlGeometryTest.cs | 35 ++++++- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs index c53030b38..9129f824d 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite( .TryAdd(p => p.GetRequiredService()) .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAddProviderSpecificServices( diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 000000000..4f22e94cf --- /dev/null +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,94 @@ +using NetTopologySuite.Algorithm; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +public class NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin +{ + public NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + if (sqlExpressionFactory is not NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory) + { + throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); + } + + Translators = new IAggregateMethodCallTranslator[] + { + new NpgsqlNetTopologySuiteAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) + }; + } + + public virtual IEnumerable Translators { get; } +} + +public class NpgsqlNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly MethodInfo GeometryCombineMethod + = typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), new[] { typeof(IEnumerable) })!; + + private static readonly MethodInfo ConvexHullMethod + = typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), new[] { typeof(IEnumerable) })!; + + private static readonly MethodInfo UnionMethod + = typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable) })!; + + private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + public NpgsqlNetTopologySuiteAggregateMethodTranslator( + NpgsqlSqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + public virtual SqlExpression? Translate( + MethodInfo method, EnumerableExpression source, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (source.Selector is not SqlExpression sqlExpression + || (method != GeometryCombineMethod && method != UnionMethod && method != ConvexHullMethod)) + { + return null; + } + + var resultTypeMapping = _typeMappingSource.FindMapping(typeof(Geometry), sqlExpression.TypeMapping?.StoreType ?? "geometry"); + + if (method == GeometryCombineMethod || method == UnionMethod) + { + return _sqlExpressionFactory.AggregateFunction( + method == GeometryCombineMethod ? "ST_Collect" : "ST_Union", + new[] { sqlExpression }, + nullable: true, + argumentsPropagateNullability: new[] { false }, + source, + typeof(Geometry), + resultTypeMapping); + } + + // PostGIS has no built-in aggregate convex hull, but we can simply apply ST_Collect beforehand as recommended in the docs + // https://postgis.net/docs/ST_ConvexHull.html + return _sqlExpressionFactory.Function( + "ST_ConvexHull", + new[] + { + _sqlExpressionFactory.AggregateFunction( + "ST_Collect", + new[] { sqlExpression }, + nullable: true, + argumentsPropagateNullability: new[] { false }, + source, + typeof(Geometry), + resultTypeMapping) + }, + nullable: true, + argumentsPropagateNullability: new[] { true }, + typeof(Geometry), + resultTypeMapping); + } +} diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs index 0058a1da3..f01d70491 100644 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -10,10 +10,11 @@ public NpgsqlNetTopologySuiteMethodCallTranslatorPlugin( IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) { - if (!(sqlExpressionFactory is NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory)) + if (sqlExpressionFactory is not NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory) { throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); } + Translators = new IMethodCallTranslator[] { new NpgsqlGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource), }; } diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs index 790caf79f..dbbadde7b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs @@ -259,8 +259,10 @@ public override async Task ToText(bool async) #region Not supported on geography public override Task Boundary(bool async) => Task.CompletedTask; + public override Task Combine_aggregate(bool async) => Task.CompletedTask; public override Task Contains(bool async) => Task.CompletedTask; public override Task ConvexHull(bool async) => Task.CompletedTask; + public override Task ConvexHull_aggregate(bool async) => Task.CompletedTask; public override Task Crosses(bool async) => Task.CompletedTask; public override Task Difference(bool async) => Task.CompletedTask; public override Task Dimension(bool async) => Task.CompletedTask; @@ -300,6 +302,7 @@ public override async Task ToText(bool async) public override Task SymmetricDifference(bool async) => Task.CompletedTask; public override Task Touches(bool async) => Task.CompletedTask; public override Task Union(bool async) => Task.CompletedTask; + public override Task Union_aggregate(bool async) => Task.CompletedTask; public override Task Union_void(bool async) => Task.CompletedTask; public override Task Within(bool async) => Task.CompletedTask; public override Task X(bool async) => Task.CompletedTask; @@ -339,4 +342,4 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build return optionsBuilder; } } -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs index 5051d1132..240f970da 100644 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -80,6 +80,17 @@ public override async Task Centroid(bool async) FROM ""PolygonEntity"" AS p"); } + public override async Task Combine_aggregate(bool async) + { + await base.Combine_aggregate(async); + + AssertSql( + @"SELECT p.""Group"" AS ""Id"", ST_Collect(p.""Point"") AS ""Combined"" +FROM ""PointEntity"" AS p +WHERE (p.""Point"" IS NOT NULL) +GROUP BY p.""Group"""); + } + public override async Task Contains(bool async) { await base.Contains(async); @@ -100,6 +111,17 @@ public override async Task ConvexHull(bool async) FROM ""PolygonEntity"" AS p"); } + public override async Task ConvexHull_aggregate(bool async) + { + await base.ConvexHull_aggregate(async); + + AssertSql( + @"SELECT p.""Group"" AS ""Id"", ST_ConvexHull(ST_Collect(p.""Point"")) AS ""ConvexHull"" +FROM ""PointEntity"" AS p +WHERE (p.""Point"" IS NOT NULL) +GROUP BY p.""Group"""); + } + public override async Task IGeometryCollection_Count(bool async) { await base.IGeometryCollection_Count(async); @@ -526,6 +548,17 @@ public override async Task Union(bool async) FROM ""PolygonEntity"" AS p"); } + public override async Task Union_aggregate(bool async) + { + await base.Union_aggregate(async); + + AssertSql( + @"SELECT p.""Group"" AS ""Id"", ST_Union(p.""Point"") AS ""Union"" +FROM ""PointEntity"" AS p +WHERE (p.""Point"" IS NOT NULL) +GROUP BY p.""Group"""); + } + public override async Task Union_void(bool async) { await base.Union_void(async); @@ -585,4 +618,4 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build return optionsBuilder; } } -} \ No newline at end of file +}