未验证 提交 aebc18ad 编写于 作者: S Shay Rojansky 提交者: GitHub

Add translation for Postgis <-> (DistanceKnn) (#1828)

Closes #1827
上级 24491d4b
......@@ -9,10 +9,9 @@ public static class NpgsqlNetTopologySuiteDbFunctionsExtensions
{
/// <summary>
/// Returns a new geometry with its coordinates transformed to a different spatial reference system.
/// Translates to <c>ST_Transform(geometry, srid)</c>.
/// </summary>
/// <remarks>
/// The method call is translated to <c>ST_Transform(geometry, srid)</c>.
///
/// See https://postgis.net/docs/ST_Transform.html.
/// </remarks>
public static TGeometry Transform<TGeometry>(this DbFunctions _, TGeometry geometry, int srid)
......@@ -21,24 +20,44 @@ public static TGeometry Transform<TGeometry>(this DbFunctions _, TGeometry geome
/// <summary>
/// Tests whether the distance from the origin geometry to another is less than or equal to a specified value.
/// Translates to <c>ST_DWithin</c>.
/// </summary>
/// <remarks>
/// See https://postgis.net/docs/ST_DWithin.html.
/// </remarks>
/// <param name="geometry">The origin geometry.</param>
/// <param name="anotherGeometry">The geometry to check the distance to.</param>
/// <param name="distance">The distance value to compare.</param>
/// <param name="useSpheroid">Whether to use sphere or spheroid distance measurement.</param>
/// <returns>True if the geometries are less than distance apart.</returns>
/// <returns><see langword="true" /> if the geometries are less than distance apart.</returns>
public static bool IsWithinDistance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, double distance, bool useSpheroid)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsWithinDistance)));
/// <summary>
/// Returns the minimum distance between the origin geometry and another geometry g.
/// Translates to <c>ST_Distance</c>.
/// </summary>
/// <remarks>
/// See https://postgis.net/docs/ST_Distance.html.
/// </remarks>
/// <param name="geometry">The origin geometry.</param>
/// <param name="anotherGeometry">The geometry from which to compute the distance.</param>
/// <param name="useSpheroid">Whether to use sphere or spheroid distance measurement.</param>
/// <returns>The distance between the geometries.</returns>
/// <exception cref="ArgumentException">If g is null</exception>
public static double Distance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, bool useSpheroid)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance)));
/// <summary>
/// Returns the 2D distance between two geometries. Used in the "ORDER BY" clause, provides index-assisted nearest-neighbor result
/// sets. Translates to <c>&lt;-&gt;</c>.
/// </summary>
/// <remarks>
/// See https://postgis.net/docs/ST_Distance.html.
/// </remarks>
/// <param name="geometry">The origin geometry.</param>
/// <param name="anotherGeometry">The geometry from which to compute the distance.</param>
/// <returns>The 2D distance between the geometries.</returns>
public static double DistanceKnn(this DbFunctions _, Geometry geometry, Geometry anotherGeometry)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceKnn)));
}
}
......@@ -9,6 +9,7 @@
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using NetTopologySuite.Geometries;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
// ReSharper disable once CheckNamespace
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
......@@ -18,10 +19,13 @@ public class NpgsqlNetTopologySuiteMethodCallTranslatorPlugin : IMethodCallTrans
public NpgsqlNetTopologySuiteMethodCallTranslatorPlugin(
IRelationalTypeMappingSource typeMappingSource,
ISqlExpressionFactory sqlExpressionFactory)
=> Translators = new IMethodCallTranslator[]
{
if (!(sqlExpressionFactory is NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory))
{
new NpgsqlGeometryMethodTranslator(sqlExpressionFactory, typeMappingSource),
};
throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory));
}
Translators = new IMethodCallTranslator[] { new NpgsqlGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource), };
}
public virtual IEnumerable<IMethodCallTranslator> Translators { get; }
}
......@@ -33,7 +37,7 @@ public class NpgsqlGeometryMethodTranslator : IMethodCallTranslator
{
private static readonly MethodInfo _collectionItem = typeof(GeometryCollection).GetRuntimeProperty("Item")!.GetMethod!;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;
private static readonly bool[][] TrueArrays =
......@@ -46,7 +50,7 @@ public class NpgsqlGeometryMethodTranslator : IMethodCallTranslator
};
public NpgsqlGeometryMethodTranslator(
ISqlExpressionFactory sqlExpressionFactory,
NpgsqlSqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
{
_sqlExpressionFactory = sqlExpressionFactory;
......@@ -78,6 +82,11 @@ public class NpgsqlGeometryMethodTranslator : IMethodCallTranslator
method.ReturnType,
arguments[1].TypeMapping),
nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.DistanceKnn) => _sqlExpressionFactory.MakePostgresBinary(
PostgresExpressionType.PostgisDistanceKnn,
arguments[1],
arguments[2]),
nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Distance) =>
TranslateGeometryMethod(arguments[1], method, new[] { arguments[2], arguments[3] }),
nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.IsWithinDistance) =>
......
......@@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using NpgsqlTypes;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
......
......@@ -8,6 +8,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
......
......@@ -12,6 +12,7 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
......
......@@ -9,6 +9,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
......
......@@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using NpgsqlTypes;
......
......@@ -127,6 +127,8 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PostgresExpressionType.JsonExistsAny => "?|",
PostgresExpressionType.JsonExistsAll => "?&",
PostgresExpressionType.PostgisDistanceKnn => "<->",
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
......
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal
{
/// <summary>
/// PostgreSQL-specific expression node types.
/// </summary>
public enum PostgresExpressionType
{
Contains,
ContainedBy,
Overlaps,
AtTimeZone,
NetworkContainedByOrEqual,
NetworkContainsOrEqual,
NetworkContainsOrContainedBy,
RangeIsStrictlyLeftOf,
RangeIsStrictlyRightOf,
RangeDoesNotExtendRightOf,
RangeDoesNotExtendLeftOf,
RangeIsAdjacentTo,
RangeUnion,
RangeIntersect,
RangeExcept,
TextSearchMatch,
TextSearchAnd,
TextSearchOr,
JsonExists,
JsonExistsAny,
JsonExistsAll,
LTreeMatches,
LTreeMatchesAny,
LTreeFirstAncestor,
LTreeFirstDescendent,
LTreeFirstMatches
}
}
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions
{
/// <summary>
/// PostgreSQL-specific expression node types.
/// </summary>
public enum PostgresExpressionType
{
Contains, // >> (inet/cidr), @>
ContainedBy, // << (inet/cidr), <@
Overlaps, // &&
AtTimeZone, // AT TIME ZONE
NetworkContainedByOrEqual, // <<=
NetworkContainsOrEqual, // >>=
NetworkContainsOrContainedBy, // &&
RangeIsStrictlyLeftOf, // <<
RangeIsStrictlyRightOf, // >>
RangeDoesNotExtendRightOf, // &<
RangeDoesNotExtendLeftOf, // &>
RangeIsAdjacentTo, // -|-
RangeUnion, // +
RangeIntersect, // *
RangeExcept, // -
TextSearchMatch, // @@
TextSearchAnd, // &&
TextSearchOr, // ||
JsonExists, // ?
JsonExistsAny, // ?@>
JsonExistsAll, // ?<@
LTreeMatches, // ~ or @
LTreeMatchesAny, // ?
LTreeFirstAncestor, // ?@>
LTreeFirstDescendent, // ?<@
LTreeFirstMatches, // ?~ or ?@
PostgisDistanceKnn // <->
}
}
......@@ -9,6 +9,7 @@
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using NpgsqlTypes;
......@@ -300,8 +301,9 @@ protected virtual Expression VisitPostgresBinary(PostgresBinaryExpression binary
PostgresExpressionType.LTreeFirstMatches
when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery" => "?@",
_ => throw new ArgumentOutOfRangeException(
$"Unhandled operator type: {binaryExpression.OperatorType}")
PostgresExpressionType.PostgisDistanceKnn => "<->",
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
......
......@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
......@@ -197,9 +198,11 @@ PostgresUnknownBinaryExpression postgresUnknownBinaryExpression
nullable = binaryExpression.OperatorType switch
{
// The following LTree search methods return null for "not found"
PostgresExpressionType.LTreeFirstAncestor => true,
PostgresExpressionType.LTreeFirstDescendent => true,
PostgresExpressionType.LTreeFirstMatches => true,
_ => leftNullable || rightNullable
};
......
......@@ -9,6 +9,7 @@
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
......@@ -18,6 +19,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly RelationalTypeMapping _boolTypeMapping;
private readonly RelationalTypeMapping _doubleTypeMapping;
private static Type? _nodaTimeDurationType;
private static Type? _nodaTimePeriodType;
......@@ -27,6 +29,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
{
_typeMappingSource = dependencies.TypeMappingSource;
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool))!;
_doubleTypeMapping = _typeMappingSource.FindMapping(typeof(double))!;
}
#region Expression factory methods
......@@ -216,6 +219,10 @@ RelationalTypeMapping FlipTimestampTypeMapping(RelationalTypeMapping mapping)
case PostgresExpressionType.JsonExistsAll:
returnType = typeof(bool);
break;
case PostgresExpressionType.PostgisDistanceKnn:
returnType = typeof(double);
break;
}
return (PostgresBinaryExpression)ApplyTypeMapping(
......@@ -508,6 +515,14 @@ private SqlExpression ApplyTypeMappingOnILike(PostgresILikeExpression ilikeExpre
break;
}
case PostgresExpressionType.PostgisDistanceKnn:
{
inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right);
resultType = typeof(double);
resultTypeMapping = _doubleTypeMapping;
break;
}
default:
throw new InvalidOperationException($"Incorrect {nameof(operatorType)} for {nameof(postgresBinaryExpression)}");
}
......
......@@ -22,7 +22,7 @@ public SpatialQueryNpgsqlGeographyTest(SpatialQueryNpgsqlGeographyFixture fixtur
: base(fixture)
{
Fixture.TestSqlLoggerFactory.Clear();
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
// Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
protected override bool AssertDistances
......@@ -92,7 +92,7 @@ public override async Task Centroid(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncDataAndUseSpheroid))]
public async Task DistanceDbFunction(bool async, bool useSpheroid)
public async Task Distance_with_spheroid(bool async, bool useSpheroid)
{
var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1));
......@@ -116,6 +116,31 @@ public async Task DistanceDbFunction(bool async, bool useSpheroid)
FROM ""PointEntity"" AS p");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task DistanceKnn(bool async)
{
var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1));
await AssertQuery(
async,
ss => ss.Set<PointEntity>().Select(e => new { e.Id, Distance = (double?)EF.Functions.DistanceKnn(e.Point, point) }),
ss => ss.Set<PointEntity>()
.Select(e => new { e.Id, Distance = (e.Point == null ? (double?)null : e.Point.Distance(point)) }),
elementSorter: e => e.Id,
elementAsserter: (e, a) =>
{
Assert.Equal(e.Id, a.Id);
Assert.Equal(e.Distance == null, a.Distance == null);
});
AssertSql(
@"@__point_1='POINT (0 1)' (DbType = Object)
SELECT p.""Id"", p.""Point"" <-> @__point_1 AS ""Distance""
FROM ""PointEntity"" AS p");
}
public override async Task GeometryType(bool async)
{
// PostGIS returns "POINT", NTS returns "Point"
......@@ -168,7 +193,7 @@ public override async Task IsWithinDistance(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncDataAndUseSpheroid))]
public async Task IsWithinDistanceDbFunction(bool async, bool useSpheroid)
public async Task IsWithinDistance_with_spheroid(bool async, bool useSpheroid)
{
var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册