Skip to content

Commit

Permalink
Translate spatial aggregates (#28115)
Browse files Browse the repository at this point in the history
Closes #13278
  • Loading branch information
roji authored Jun 21, 2022
1 parent 5ff4696 commit e257d1d
Show file tree
Hide file tree
Showing 20 changed files with 488 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ private static void AppendLiteral(StoreObjectIdentifier storeObject, IndentedStr
.Append("DbFunction(").Append(code.Literal(storeObject.Name)).Append(")");
break;
default:
Check.DebugAssert(false, "Unexpected StoreObjectType: " + storeObject.StoreObjectType);
Check.DebugFail("Unexpected StoreObjectType: " + storeObject.StoreObjectType);
break;
}
}
Expand Down
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 @@ -237,7 +237,7 @@ protected virtual IReadOnlyList<MigrationOperation> Sort(
else
{
leftovers.Add(operation);
Check.DebugAssert(false, "Unexpected operation type: " + operation.GetType());
Check.DebugFail("Unexpected operation type: " + operation.GetType());
}
}

Expand Down Expand Up @@ -275,7 +275,7 @@ protected virtual IReadOnlyList<MigrationOperation> Sort(
}
else
{
Check.DebugAssert(false, "Operation removed twice: " + cyclicAddForeignKeyOperation);
Check.DebugFail("Operation removed twice: " + cyclicAddForeignKeyOperation);
}
}
Expand Down Expand Up @@ -2189,7 +2189,7 @@ private IEnumerable<MigrationOperation> GetDataOperations(

if (forSource)
{
Check.DebugAssert(false, "Insert using the source model");
Check.DebugFail("Insert using the source model");
break;
}

Expand All @@ -2212,7 +2212,7 @@ private IEnumerable<MigrationOperation> GetDataOperations(

if (forSource)
{
Check.DebugAssert(false, "Update using the source model");
Check.DebugFail("Update using the source model");
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public virtual Expression Expand(
return _visitedFromSqlExpressions[fromSql] = fromSql.Update(Expression.Constant(constantValues, typeof(object[])));

default:
Check.DebugAssert(false, "FromSql.Arguments must be Constant/ParameterExpression");
Check.DebugFail("FromSql.Arguments must be Constant/ParameterExpression");
return null;
}
}
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>()
.TryAdd<IEvaluatableExpressionFilterPlugin, SqlServerNetTopologySuiteEvaluatableExpressionFilterPlugin>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ public SqlServerGeometryMethodTranslator(
arguments.Where(e => typeof(Geometry).IsAssignableFrom(e.Type)));
var typeMapping = ExpressionExtensions.InferTypeMapping(geometryExpressions.ToArray());

Check.DebugAssert(typeMapping != null, "At least one argument must have typeMapping.");
if (typeMapping is null)
{
Check.DebugFail("At least one argument must have typeMapping.");
return null;
}

var storeType = typeMapping.StoreType;
var isGeography = string.Equals(storeType, "geography", StringComparison.OrdinalIgnoreCase);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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(
IRelationalTypeMappingSource typeMappingSource,
ISqlExpressionFactory sqlExpressionFactory)
{
Translators = new IAggregateMethodCallTranslator[]
{
new SqlServerNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory, 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 virtual IEnumerable<IAggregateMethodCallTranslator> Translators { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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;
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 SqlServerNetTopologySuiteAggregateMethodTranslator(
ISqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
{
_sqlExpressionFactory = sqlExpressionFactory;
_typeMappingSource = 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 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;
}

if (sqlExpression.TypeMapping is not { } typeMapping)
{
Check.DebugFail("SQL expression is missing a type mapping.");
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(
$"{typeMapping.StoreType}::{functionName}",
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { false },
method.ReturnType,
_typeMappingSource.FindMapping(method.ReturnType, typeMapping.StoreType));
}
}
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,37 @@
// 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,
IRelationalTypeMappingSource typeMappingSource)
{
Translators = new IAggregateMethodCallTranslator[]
{
new SqliteNetTopologySuiteAggregateMethodTranslator(sqlExpressionFactory, 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 virtual IEnumerable<IAggregateMethodCallTranslator> Translators { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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;
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;
}

/// <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(
"ConvexHull",
new[]
{
_sqlExpressionFactory.Function(
"Collect",
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { false },
typeof(Geometry))
},
nullable: true,
argumentsPropagateNullability: new[] { true },
typeof(Geometry));
}
}
Loading

0 comments on commit e257d1d

Please sign in to comment.