From 913c215ce465a5161e3b2408087b2b4975be91b0 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 26 May 2022 17:13:51 +0200 Subject: [PATCH] Translate spatial aggregates Closes #13278 --- ...ntityFrameworkRelationalServicesBuilder.cs | 1 + ...opologySuiteServiceCollectionExtensions.cs | 1 + ...uiteAggregateMethodCallTranslatorPlugin.cs | 36 +++++++ ...tTopologySuiteAggregateMethodTranslator.cs | 90 ++++++++++++++++++ ...opologySuiteServiceCollectionExtensions.cs | 1 + ...uiteAggregateMethodCallTranslatorPlugin.cs | 36 +++++++ ...tTopologySuiteAggregateMethodTranslator.cs | 95 +++++++++++++++++++ .../Query/SpatialQueryTestBase.cs | 75 +++++++++++++++ .../TestModels/SpatialModel/PointEntity.cs | 1 + .../TestModels/SpatialModel/SpatialData.cs | 23 ++++- .../SpatialQuerySqlServerGeographyTest.cs | 15 ++- .../SpatialQuerySqlServerGeometryTest.cs | 39 +++++++- .../Query/SpatialQuerySqliteTest.cs | 37 +++++++- 13 files changed, 441 insertions(+), 9 deletions(-) create mode 100644 src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs create mode 100644 src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodTranslator.cs create mode 100644 src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs create mode 100644 src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodTranslator.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index a30b3485fb7..a20a9fca65f 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -90,6 +90,7 @@ public static readonly IDictionary RelationalServi new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, { typeof(IMethodCallTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, + { typeof(IAggregateMethodCallTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) } }; diff --git a/src/EFCore.SqlServer.NTS/Extensions/SqlServerNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.SqlServer.NTS/Extensions/SqlServerNetTopologySuiteServiceCollectionExtensions.cs index 36ef4579b1c..22318cf3fdf 100644 --- a/src/EFCore.SqlServer.NTS/Extensions/SqlServerNetTopologySuiteServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer.NTS/Extensions/SqlServerNetTopologySuiteServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ public static IServiceCollection AddEntityFrameworkSqlServerNetTopologySuite( new EntityFrameworkRelationalServicesBuilder(serviceCollection) .TryAdd() .TryAdd() + .TryAdd() .TryAdd(); return serviceCollection; diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 00000000000..be24834c5d3 --- /dev/null +++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin( + ISqlExpressionFactory sqlExpressionFactory) + { + Translators = new IAggregateMethodCallTranslator[] + { + new SqlServerNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory) + }; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public IEnumerable Translators { get; } +} diff --git a/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodTranslator.cs new file mode 100644 index 00000000000..ab8a66f90ad --- /dev/null +++ b/src/EFCore.SqlServer.NTS/Query/Internal/SqlServerNetTopologySuiteAggregateMethodTranslator.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using NetTopologySuite.Algorithm; +using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerNetTopologySuiteAggregateMethodTranslator : 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 ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerNetTopologySuiteAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + => _sqlExpressionFactory = sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + MethodInfo method, EnumerableExpression source, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://docs.microsoft.com/sql/t-sql/spatial-geometry/static-aggregate-geometry-methods + + if (source.Selector is not SqlExpression sqlExpression) + { + return null; + } + + var functionName = method == GeometryCombineMethod + ? "CollectionAggregate" + : method == UnionMethod + ? "UnionAggregate" + : method == ConvexHullMethod + ? "ConvexHullAggregate" + : null; + + if (functionName is null) + { + return null; + } + + if (source.Predicate != null) + { + sqlExpression = _sqlExpressionFactory.Case( + new List { new(source.Predicate, sqlExpression) }, + elseResult: null); + } + + if (source.IsDistinct) + { + sqlExpression = new DistinctExpression(sqlExpression); + } + + return _sqlExpressionFactory.Function( + "geometry::" + functionName, + new[] { sqlExpression }, + nullable: true, + argumentsPropagateNullability: new[] { false }, + typeof(Geometry), + sqlExpression.TypeMapping); + } +} diff --git a/src/EFCore.Sqlite.NTS/Extensions/SqliteNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.NTS/Extensions/SqliteNetTopologySuiteServiceCollectionExtensions.cs index d0d9f50829b..7c9566c91eb 100644 --- a/src/EFCore.Sqlite.NTS/Extensions/SqliteNetTopologySuiteServiceCollectionExtensions.cs +++ b/src/EFCore.Sqlite.NTS/Extensions/SqliteNetTopologySuiteServiceCollectionExtensions.cs @@ -30,6 +30,7 @@ public static IServiceCollection AddEntityFrameworkSqliteNetTopologySuite( new EntityFrameworkRelationalServicesBuilder(serviceCollection) .TryAdd() .TryAdd() + .TryAdd() .TryAdd(); return serviceCollection; diff --git a/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 00000000000..3d0633310e3 --- /dev/null +++ b/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin( + ISqlExpressionFactory sqlExpressionFactory) + { + Translators = new IAggregateMethodCallTranslator[] + { + new SqliteNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory) + }; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public IEnumerable Translators { get; } +} diff --git a/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodTranslator.cs b/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodTranslator.cs new file mode 100644 index 00000000000..20f34cff291 --- /dev/null +++ b/src/EFCore.Sqlite.NTS/Query/Internal/SqliteNetTopologySuiteAggregateMethodTranslator.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using NetTopologySuite.Algorithm; +using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqliteNetTopologySuiteAggregateMethodTranslator : 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 ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqliteNetTopologySuiteAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + => _sqlExpressionFactory = sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + 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; + } + + if (source.Predicate != null) + { + sqlExpression = _sqlExpressionFactory.Case( + new List { new(source.Predicate, sqlExpression) }, + elseResult: null); + } + + if (source.IsDistinct) + { + sqlExpression = new DistinctExpression(sqlExpression); + } + + if (method == GeometryCombineMethod || method == UnionMethod) + { + return _sqlExpressionFactory.Function( + method == GeometryCombineMethod ? "Collect" : "GUnion", + new[] { sqlExpression }, + nullable: true, + argumentsPropagateNullability: new[] { false }, + typeof(Geometry)); + } + + // Spatialite has no built-in aggregate convex hull, but we can simply apply Collect beforehand + return _sqlExpressionFactory.Function( + "ST_ConvexHull", + new[] + { + _sqlExpressionFactory.Function( + "Collect", + new[] { sqlExpression }, + nullable: true, + argumentsPropagateNullability: new[] { false }, + typeof(Geometry)) + }, + nullable: true, + argumentsPropagateNullability: new[] { true }, + typeof(Geometry), + sqlExpression.TypeMapping); + } +} diff --git a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs index aae53435bd9..6319dbbe937 100644 --- a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs @@ -3,6 +3,9 @@ using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Polygonize; +using NetTopologySuite.Operation.Union; namespace Microsoft.EntityFrameworkCore.Query; @@ -179,6 +182,32 @@ public virtual Task Centroid(bool async) Assert.Equal(e.Centroid, a.Centroid, GeometryComparer.Instance); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Combine_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.Point != null) + .GroupBy(e => e.Group) + .Select(g => new + { + Id = g.Key, + Combined = GeometryCombiner.Combine(g.Select(e => e.Point)) + }), + elementSorter: x => x.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + + // Note that NTS returns a MultiPoint (which is a subclass of GeometryCollection), whereas SQL Server returns a + // GeometryCollection. + var eCollection = (GeometryCollection)e.Combined; + var aCollection = (GeometryCollection)a.Combined; + + Assert.Equal(eCollection.Geometries, aCollection.Geometries); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Contains(bool async) @@ -207,6 +236,27 @@ public virtual Task ConvexHull(bool async) Assert.Equal(e.ConvexHull, a.ConvexHull, GeometryComparer.Instance); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ConvexHull_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.Point != null) + .GroupBy(e => e.Group) + .Select(g => new { Id = g.Key, ConvexHull = NetTopologySuite.Algorithm.ConvexHull.Create(g.Select(e => e.Point)) }), + elementSorter: x => x.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + + var eLineString = (LineString)e.ConvexHull; + var aLineString = (LineString)a.ConvexHull; + + // The ordering of the points within the linestring is non-deterministic + Assert.Equal(eLineString.Coordinates.OrderBy(p => p), aLineString.Coordinates.OrderBy(p => p)); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task IGeometryCollection_Count(bool async) @@ -1066,6 +1116,31 @@ public virtual Task Union(bool async) }); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.Point != null) + .GroupBy(e => e.Group) + .Select(g => new + { + Id = g.Key, + Union = UnaryUnionOp.Union(g.Select(e => e.Point)) + }), + elementSorter: x => x.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + + var eCollection = (GeometryCollection)e.Union; + var aCollection = (GeometryCollection)a.Union; + + // The ordering of the points within the union is non-deterministic + Assert.Equal(eCollection.Geometries.OrderBy(p => p), aCollection.Geometries.OrderBy(p => p)); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Union_void(bool async) diff --git a/test/EFCore.Specification.Tests/TestModels/SpatialModel/PointEntity.cs b/test/EFCore.Specification.Tests/TestModels/SpatialModel/PointEntity.cs index 3acca73d652..af2e4b5067f 100644 --- a/test/EFCore.Specification.Tests/TestModels/SpatialModel/PointEntity.cs +++ b/test/EFCore.Specification.Tests/TestModels/SpatialModel/PointEntity.cs @@ -10,6 +10,7 @@ public class PointEntity public static readonly Guid WellKnownId = Guid.Parse("2F39AADE-4D8D-42D2-88CE-775C84AB83B1"); public Guid Id { get; set; } + public string Group { get; set; } public Geometry Geometry { get; set; } public Point Point { get; set; } public Point PointZ { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/SpatialModel/SpatialData.cs b/test/EFCore.Specification.Tests/TestModels/SpatialModel/SpatialData.cs index cc11858c487..94ca37ca12f 100644 --- a/test/EFCore.Specification.Tests/TestModels/SpatialModel/SpatialData.cs +++ b/test/EFCore.Specification.Tests/TestModels/SpatialModel/SpatialData.cs @@ -60,12 +60,27 @@ public static IReadOnlyList CreatePointEntities(GeometryFactory fac new PointEntity { Id = PointEntity.WellKnownId, + Group = "A", Point = factory.CreatePoint(new Coordinate(0, 0)), PointZ = factory.CreatePoint(new CoordinateZ(0, 0, 0)), PointM = factory.CreatePoint(new CoordinateM(0, 0, 0)), PointZM = factory.CreatePoint(new CoordinateZM(0, 0, 0, 0)) }, - new PointEntity { Id = Guid.Parse("67A54C9B-4C3B-4B27-8B4E-C0335E50E551"), Point = null } + new PointEntity + { + Id = Guid.Parse("2F39AADE-4D8D-42D2-88CE-775C84AB83B2"), + Group = "A", + Point = factory.CreatePoint(new Coordinate(1, 1)), + PointZ = factory.CreatePoint(new CoordinateZ(1, 1, 1)), + PointM = factory.CreatePoint(new CoordinateM(1, 1, 1)), + PointZM = factory.CreatePoint(new CoordinateZM(1, 1, 1, 1)) + }, + new PointEntity + { + Id = Guid.Parse("67A54C9B-4C3B-4B27-8B4E-C0335E50E551"), + Group = "B", + Point = null + } }; foreach (var entity in entities) @@ -110,7 +125,11 @@ public static IReadOnlyList CreatePolygonEntities(GeometryFactory Polygon = factory.CreatePolygon( new[] { new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0) }) }, - new PolygonEntity { Id = Guid.Parse("F1B00CB9-862B-417B-955A-F1F7688B2AB5"), Polygon = null } + new PolygonEntity + { + Id = Guid.Parse("F1B00CB9-862B-417B-955A-F1F7688B2AB5"), + Polygon = null + } }; public static IReadOnlyList CreateMultiLineStringEntities(GeometryFactory factory) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs index 26b4b7f1dc0..69d4bdda398 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs @@ -24,7 +24,7 @@ public override async Task SimpleSelect(bool async) await base.SimpleSelect(async); AssertSql( - @"SELECT [p].[Id], [p].[Geometry], [p].[Point], [p].[PointM], [p].[PointZ], [p].[PointZM] + @"SELECT [p].[Id], [p].[Geometry], [p].[Group], [p].[Point], [p].[PointM], [p].[PointZ], [p].[PointZM] FROM [PointEntity] AS [p]", // @"SELECT [l].[Id], [l].[LineString] @@ -106,6 +106,10 @@ public override Task Buffer_quadrantSegments(bool async) public override Task Centroid(bool async) => Task.CompletedTask; + // No SqlServer Translation + public override Task Combine_aggregate(bool async) + => Task.CompletedTask; + public override async Task Contains(bool async) { await base.Contains(async); @@ -126,6 +130,9 @@ public override async Task ConvexHull(bool async) FROM [PolygonEntity] AS [p]"); } + public override Task ConvexHull_aggregate(bool async) + => Task.CompletedTask; + public override async Task IGeometryCollection_Count(bool async) { await base.IGeometryCollection_Count(async); @@ -614,6 +621,10 @@ public override async Task Union(bool async) FROM [PolygonEntity] AS [p]"); } + // No SqlServer Translation + public override Task Union_aggregate(bool async) + => Task.CompletedTask; + // No SqlServer Translation public override Task Union_void(bool async) => Task.CompletedTask; @@ -661,7 +672,7 @@ public override async Task XY_with_collection_join(bool async) await base.XY_with_collection_join(async); AssertSql( - @"SELECT [t].[Id], [t].[c], [t].[c0], [p0].[Id], [p0].[Geometry], [p0].[Point], [p0].[PointM], [p0].[PointZ], [p0].[PointZM] + @"SELECT [t].[Id], [t].[c], [t].[c0], [p0].[Id], [p0].[Geometry], [p0].[Group], [p0].[Point], [p0].[PointM], [p0].[PointZ], [p0].[PointZM] FROM ( SELECT TOP(1) [p].[Id], [p].[Point].Long AS [c], [p].[Point].Lat AS [c0] FROM [PointEntity] AS [p] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs index 9d024791595..23a00906d10 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs @@ -12,7 +12,7 @@ public SpatialQuerySqlServerGeometryTest(SpatialQuerySqlServerGeometryFixture fi : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } protected override bool CanExecuteQueryString @@ -23,7 +23,7 @@ public override async Task SimpleSelect(bool async) await base.SimpleSelect(async); AssertSql( - @"SELECT [p].[Id], [p].[Geometry], [p].[Point], [p].[PointM], [p].[PointZ], [p].[PointZM] + @"SELECT [p].[Id], [p].[Geometry], [p].[Group], [p].[Point], [p].[PointM], [p].[PointZ], [p].[PointZM] FROM [PointEntity] AS [p]", // @"SELECT [l].[Id], [l].[LineString] @@ -115,6 +115,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], geometry::CollectionAggregate([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); @@ -135,6 +146,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], geometry::ConvexHullAggregate([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); @@ -699,6 +721,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], geometry::UnionAggregate([p].[Point]) AS [Union] +FROM [PointEntity] AS [p] +WHERE [p].[Point] IS NOT NULL +GROUP BY [p].[Group]"); + } + // No SqlServer Translation public override Task Union_void(bool async) => Task.CompletedTask; @@ -746,7 +779,7 @@ public override async Task XY_with_collection_join(bool async) await base.XY_with_collection_join(async); AssertSql( - @"SELECT [t].[Id], [t].[c], [t].[c0], [p0].[Id], [p0].[Geometry], [p0].[Point], [p0].[PointM], [p0].[PointZ], [p0].[PointZM] + @"SELECT [t].[Id], [t].[c], [t].[c0], [p0].[Id], [p0].[Geometry], [p0].[Group], [p0].[Point], [p0].[PointM], [p0].[PointZ], [p0].[PointZM] FROM ( SELECT TOP(1) [p].[Id], [p].[Point].STX AS [c], [p].[Point].STY AS [c0] FROM [PointEntity] AS [p] diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs index 31bdaa4ab45..67316a9a2b4 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs @@ -10,7 +10,7 @@ public SpatialQuerySqliteTest(SpatialQuerySqliteFixture fixture, ITestOutputHelp : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } public override async Task SimpleSelect(bool async) @@ -18,7 +18,7 @@ public override async Task SimpleSelect(bool async) await base.SimpleSelect(async); AssertSql( - @"SELECT ""p"".""Id"", ""p"".""Geometry"", ""p"".""Point"", ""p"".""PointM"", ""p"".""PointZ"", ""p"".""PointZM"" + @"SELECT ""p"".""Id"", ""p"".""Geometry"", ""p"".""Group"", ""p"".""Point"", ""p"".""PointM"", ""p"".""PointZ"", ""p"".""PointZM"" FROM ""PointEntity"" AS ""p""", // @"SELECT ""l"".""Id"", ""l"".""LineString"" @@ -155,6 +155,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"", 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); @@ -177,6 +188,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(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); @@ -749,6 +771,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"", GUnion(""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);