Skip to content

Commit

Permalink
Implement spatial EnvelopeCombine aggregate translation (#28394)
Browse files Browse the repository at this point in the history
Closes #28184
  • Loading branch information
roji authored Jul 8, 2022
1 parent 7c90861 commit 94d4a1e
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ private static readonly MethodInfo ConvexHullMethod
private static readonly MethodInfo UnionMethod
= typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo EnvelopeCombineMethod
= typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), new[] { typeof(IEnumerable<Geometry>) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;

Expand Down Expand Up @@ -72,7 +75,9 @@ public SqlServerNetTopologySuiteAggregateMethodTranslator(
? "UnionAggregate"
: method == ConvexHullMethod
? "ConvexHullAggregate"
: null;
: method == EnvelopeCombineMethod
? "EnvelopeAggregate"
: null;

if (functionName is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ public class SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggreg
/// 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.
/// </summary>
public SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin(
ISqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
public SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin(ISqlExpressionFactory sqlExpressionFactory)
{
Translators = new IAggregateMethodCallTranslator[]
{
new SqliteNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource)
new SqliteNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,19 @@ private static readonly MethodInfo ConvexHullMethod
private static readonly MethodInfo UnionMethod
= typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo EnvelopeCombineMethod
= typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), new[] { typeof(IEnumerable<Geometry>) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;

/// <summary>
/// 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.
/// </summary>
public SqliteNetTopologySuiteAggregateMethodTranslator(
ISqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
{
_sqlExpressionFactory = sqlExpressionFactory;
_typeMappingSource = typeMappingSource;
}
public SqliteNetTopologySuiteAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
=> _sqlExpressionFactory = sqlExpressionFactory;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -53,48 +50,67 @@ public SqliteNetTopologySuiteAggregateMethodTranslator(
MethodInfo method, EnumerableExpression source, IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (source.Selector is not SqlExpression sqlExpression
|| (method != GeometryCombineMethod && method != UnionMethod && method != ConvexHullMethod))
if (source.Selector is not SqlExpression sqlExpression)
{
return null;
}

if (source.Predicate != null)
if (method == ConvexHullMethod)
{
sqlExpression = _sqlExpressionFactory.Case(
new List<CaseWhenClause> { new(source.Predicate, sqlExpression) },
elseResult: null);
}
CombineAggregateTerms();

if (source.IsDistinct)
{
sqlExpression = new DistinctExpression(sqlExpression);
}

if (method == GeometryCombineMethod || method == UnionMethod)
{
// Spatialite has no built-in aggregate convex hull, but we can simply apply Collect beforehand
return _sqlExpressionFactory.Function(
method == GeometryCombineMethod ? "Collect" : "GUnion",
new[] { sqlExpression },
"ConvexHull",
new[]
{
_sqlExpressionFactory.Function(
"Collect",
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { false },
typeof(Geometry))
},
nullable: true,
argumentsPropagateNullability: new[] { false },
argumentsPropagateNullability: new[] { true },
typeof(Geometry));
}

// Spatialite has no built-in aggregate convex hull, but we can simply apply Collect beforehand
var functionName = method == UnionMethod
? "GUnion"
: method == GeometryCombineMethod
? "Collect"
: method == EnvelopeCombineMethod
? "Extent"
: null;

if (functionName is null)
{
return null;
}

CombineAggregateTerms();

return _sqlExpressionFactory.Function(
"ConvexHull",
new[]
{
_sqlExpressionFactory.Function(
"Collect",
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { false },
typeof(Geometry))
},
functionName,
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { true },
argumentsPropagateNullability: new[] { false },
typeof(Geometry));

void CombineAggregateTerms()
{
if (source.Predicate != null)
{
sqlExpression = _sqlExpressionFactory.Case(
new List<CaseWhenClause> { new(source.Predicate, sqlExpression) },
elseResult: null);
}

if (source.IsDistinct)
{
sqlExpression = new DistinctExpression(sqlExpression);
}
}
}
}
16 changes: 16 additions & 0 deletions test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,22 @@ public virtual Task Combine_aggregate(bool async)
Assert.Equal(eCollection.Geometries, aCollection.Geometries);
});

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task EnvelopeCombine_aggregate(bool async)
=> AssertQuery(
async,
ss => ss.Set<PointEntity>()
.Where(e => e.Point != null)
.GroupBy(e => e.Group)
.Select(g => new { Id = g.Key, Combined = EnvelopeCombiner.CombineAsGeometry(g.Select(e => e.Point)) }),
elementSorter: x => x.Id,
elementAsserter: (e, a) =>
{
Assert.Equal(e.Id, a.Id);
Assert.Equal(e.Combined, a.Combined, GeometryComparer.Instance);
});

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Contains(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.TestModels.SpatialModel;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO;

namespace Microsoft.EntityFrameworkCore.Query;

Expand Down Expand Up @@ -120,6 +121,10 @@ WHERE [p].[Point] IS NOT NULL
GROUP BY [p].[Group]");
}

// SQL Server returns a CurvePolygon, https://github.com/NetTopologySuite/NetTopologySuite.IO.SqlServerBytes/issues/18
public override async Task EnvelopeCombine_aggregate(bool async)
=> await Assert.ThrowsAsync<ParseException>(() => base.EnvelopeCombine_aggregate(async));

public override async Task Contains(bool async)
{
await base.Contains(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ WHERE [p].[Point] IS NOT NULL
GROUP BY [p].[Group]");
}

public override async Task EnvelopeCombine_aggregate(bool async)
{
await base.EnvelopeCombine_aggregate(async);

AssertSql(
@"SELECT [p].[Group] AS [Id], geometry::EnvelopeAggregate([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);
Expand Down
11 changes: 11 additions & 0 deletions test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ public override async Task Combine_aggregate(bool async)
GROUP BY ""p"".""Group""");
}

public override async Task EnvelopeCombine_aggregate(bool async)
{
await base.EnvelopeCombine_aggregate(async);

AssertSql(
@"SELECT ""p"".""Group"" AS ""Id"", Extent(""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);
Expand Down

0 comments on commit 94d4a1e

Please sign in to comment.