Skip to content

Commit

Permalink
Translate spatial aggregates
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed May 27, 2022
1 parent f5d339b commit 2f06316
Show file tree
Hide file tree
Showing 13 changed files with 434 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> 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) }
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static IServiceCollection AddEntityFrameworkSqlServerNetTopologySuite(
new EntityFrameworkRelationalServicesBuilder(serviceCollection)
.TryAdd<IRelationalTypeMappingSourcePlugin, SqlServerNetTopologySuiteTypeMappingSourcePlugin>()
.TryAdd<IMethodCallTranslatorPlugin, SqlServerNetTopologySuiteMethodCallTranslatorPlugin>()
.TryAdd<IAggregateMethodCallTranslatorPlugin, SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin>()
.TryAdd<IMemberTranslatorPlugin, SqlServerNetTopologySuiteMemberTranslatorPlugin>();

return serviceCollection;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <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 class SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin
{
/// <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 SqlServerNetTopologySuiteAggregateMethodCallTranslatorPlugin(
ISqlExpressionFactory sqlExpressionFactory)
{
Translators = new IAggregateMethodCallTranslator[]
{
new SqlServerNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory)
};
}

/// <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 IEnumerable<IAggregateMethodCallTranslator> Translators { get; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <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 class SqlServerNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator
{
private static readonly MethodInfo GeometryCombineMethod
= typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo ConvexHullMethod
= typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo UnionMethod
= typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable<Geometry>) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;

/// <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 SqlServerNetTopologySuiteAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
=> _sqlExpressionFactory = sqlExpressionFactory;

/// <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 virtual SqlExpression? Translate(
MethodInfo method, EnumerableExpression source, IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> 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<CaseWhenClause> { 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static IServiceCollection AddEntityFrameworkSqliteNetTopologySuite(
new EntityFrameworkRelationalServicesBuilder(serviceCollection)
.TryAdd<IRelationalTypeMappingSourcePlugin, SqliteNetTopologySuiteTypeMappingSourcePlugin>()
.TryAdd<IMethodCallTranslatorPlugin, SqliteNetTopologySuiteMethodCallTranslatorPlugin>()
.TryAdd<IAggregateMethodCallTranslatorPlugin, SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin>()
.TryAdd<IMemberTranslatorPlugin, SqliteNetTopologySuiteMemberTranslatorPlugin>();

return serviceCollection;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <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 class SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin
{
/// <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 SqliteNetTopologySuiteAggregateMethodCallTranslatorPlugin(
ISqlExpressionFactory sqlExpressionFactory)
{
Translators = new IAggregateMethodCallTranslator[]
{
new SqliteNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory)
};
}

/// <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 IEnumerable<IAggregateMethodCallTranslator> Translators { get; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <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 class SqliteNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator
{
private static readonly MethodInfo GeometryCombineMethod
= typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo ConvexHullMethod
= typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), new[] { typeof(IEnumerable<Geometry>) })!;

private static readonly MethodInfo UnionMethod
= typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), new[] { typeof(IEnumerable<Geometry>) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;

/// <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)
=> _sqlExpressionFactory = sqlExpressionFactory;

/// <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 virtual SqlExpression? Translate(
MethodInfo method, EnumerableExpression source, IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> 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<CaseWhenClause> { 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);
}
}
75 changes: 75 additions & 0 deletions test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<PointEntity>()
.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)
Expand Down Expand Up @@ -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<PointEntity>()
.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)
Expand Down Expand Up @@ -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<PointEntity>()
.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading

0 comments on commit 2f06316

Please sign in to comment.