From e8541293e5d3b47d83aef3187e3b80a1b289e4b8 Mon Sep 17 00:00:00 2001 From: John Gross Date: Sat, 8 Jul 2023 20:15:23 -0500 Subject: [PATCH 1/2] Initial implementation of cube support --- .../NpgsqlCubeDbFunctionsExtensions.cs | 122 ++++++++++++++++++ .../Internal/NpgsqlCubeTranslator.cs | 61 +++++++++ .../NpgsqlMethodCallTranslatorProvider.cs | 1 + .../Expressions/PostgresExpressionType.cs | 24 ++++ .../Query/Internal/NpgsqlQuerySqlGenerator.cs | 5 + .../Internal/Mapping/NpgsqlCubeTypeMapping.cs | 52 ++++++++ .../Internal/NpgsqlTypeMappingSource.cs | 3 + .../Query/CubeQueryNpgsqlTest.cs | 78 +++++++++++ 8 files changed, 346 insertions(+) create mode 100644 src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs new file mode 100644 index 000000000..fcdce89a4 --- /dev/null +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs @@ -0,0 +1,122 @@ +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods for supporting PostgreSQL translation. +/// +public static class NpgsqlCubeDbFunctionsExtensions +{ + /// + /// Determines whether a cube overlaps with a specified cube. + /// + /// + /// + /// + /// true if the cubes overlap; otherwise, false. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Overlaps(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + /// + /// Determines whether a cube contains a specified cube. + /// + /// The cube in which to locate the specified cube. + /// The specified cube to locate in the cube. + /// + /// true if the cube contains the specified cube; otherwise, false. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Contains(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a cube is contained by a specified cube. + /// + /// The cube to locate in the specified cube. + /// The specified cube in which to locate the cube. + /// + /// true if the cube is contained by the specified cube; otherwise, false. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainedBy(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Extracts the n-th coordinate of the cube (counting from 1). + /// + /// The cube from which to extract the specified coordinate. + /// The specified coordinate to extract from the cube. + /// + /// The n-th coordinate of the cube (counting from 1). + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static double NthCoordinate(this NpgsqlCube cube, int n) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate))); + + /// + /// Extracts the n-th coordinate of the cube, counting in the following way: n = 2 * k - 1 means lower bound + /// of k-th dimension, n = 2 * k means upper bound of k-th dimension. Negative n denotes the inverse value + /// of the corresponding positive coordinate. This operator is designed for KNN-GiST support. + /// + /// The cube from which to extract the specified coordinate. + /// The specified coordinate to extract from the cube. + /// + /// The n-th coordinate of the cube, counting in the following way: n = 2 * k - 1. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static double NthCoordinate2(this NpgsqlCube cube, int n) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate2))); + + /// + /// Computes the Euclidean distance between two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The Euclidean distance between the specified cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static double Distance(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Computes the taxicab (L-1 metric) distance between two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The taxicab (L-1 metric) distance between the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static double DistanceTaxicab(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab))); + + /// + /// Computes the Chebyshev (L-inf metric) distance between two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The Chebyshev (L-inf metric) distance between the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static double DistanceChebyshev(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev))); +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs new file mode 100644 index 000000000..d0c2b1c34 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs @@ -0,0 +1,61 @@ +using System.Security.AccessControl; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlCubeTranslator : IMethodCallTranslator +{ + private readonly NpgsqlSqlExpressionFactory _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 NpgsqlCubeTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions)) + { + return null; + } + + return method.Name switch + { + nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) + => _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.Contains) + => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) + => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate2) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate2, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.Distance) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.Distance, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceTaxicab, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceChebyshev, arguments[0], arguments[1]), + + _ => null + }; + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index f688eed2b..d265a5f72 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -63,6 +63,7 @@ public NpgsqlMethodCallTranslatorProvider( new NpgsqlRowValueTranslator(sqlExpressionFactory), new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory, model), new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), + new NpgsqlCubeTranslator(sqlExpressionFactory), }); } } diff --git a/src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs b/src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs index 967b3b1aa..bb0f3a337 100644 --- a/src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs +++ b/src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs @@ -159,4 +159,28 @@ public enum PostgresExpressionType LTreeFirstMatches, // ?~ or ?@ #endregion LTree + + #region Cube + + /// + /// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (counting from 1). + /// + CubeNthCoordinate, // -> + + /// + /// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (by n = 2 * k - 1). + /// + CubeNthCoordinate2, // ~> + + /// + /// Represents a PostgreSQL operator for computing the taxicab (L-1 metric) distance between two cubes. + /// + CubeDistanceTaxicab, // <#> + + /// + /// Represents a PostgreSQL operator for computing the Chebyshev (L-inf metric) distance between two cubes. + /// + CubeDistanceChebyshev, // <=> + + #endregion } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 05ad44e8c..1fe415b0e 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -512,6 +512,11 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp PostgresExpressionType.Distance => "<->", + PostgresExpressionType.CubeNthCoordinate => "->", + PostgresExpressionType.CubeNthCoordinate2 => "~>", + PostgresExpressionType.CubeDistanceTaxicab => "<#>", + PostgresExpressionType.CubeDistanceChebyshev => "<=>", + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}") }) .Append(" "); diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs new file mode 100644 index 000000000..5adc3ee49 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs @@ -0,0 +1,52 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +/// +/// 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 NpgsqlCubeTypeMapping : NpgsqlTypeMapping +{ + /// + /// 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 NpgsqlCubeTypeMapping() : base("cube", typeof(NpgsqlCube), NpgsqlDbType.Cube) {} + + /// + /// 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. + /// + protected NpgsqlCubeTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, NpgsqlDbType.Cube) {} + + /// + /// 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. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlCubeTypeMapping(parameters); + + /// + /// Generates the SQL representation of a non-null literal value. + /// + /// The literal value. + /// The generated string. + protected override string GenerateNonNullSqlLiteral(object value) + { + if (!(value is NpgsqlCube cube)) + throw new InvalidOperationException($"Can't generate a cube SQL literal for CLR type {value.GetType()}"); + + if (cube.Point) + return $"'({string.Join(",", cube.LowerLeft)})'::cube"; + else + return $"'({string.Join(",", cube.LowerLeft)}),({string.Join(",", cube.UpperRight)})'::cube"; + } +} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index c398233ce..02db6b1fe 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -183,6 +183,7 @@ static NpgsqlTypeMappingSource() private readonly NpgsqlHstoreTypeMapping _immutableHstore = new(typeof(ImmutableDictionary)); private readonly NpgsqlTidTypeMapping _tid = new(); private readonly NpgsqlPgLsnTypeMapping _pgLsn = new(); + private readonly NpgsqlCubeTypeMapping _cube = new(); private readonly NpgsqlLTreeTypeMapping _ltree = new(); private readonly NpgsqlStringTypeMapping _ltreeString = new("ltree", NpgsqlDbType.LTree); @@ -336,6 +337,7 @@ public NpgsqlTypeMappingSource( { "lo", new[] { _lo } }, { "tid", new[] { _tid } }, { "pg_lsn", new[] { _pgLsn } }, + { "cube", new[] { _cube } }, { "int4range", new[] { _int4range } }, { "int8range", new[] { _int8range } }, @@ -397,6 +399,7 @@ public NpgsqlTypeMappingSource( { typeof(Dictionary), _hstore }, { typeof(NpgsqlTid), _tid }, { typeof(NpgsqlLogSequenceNumber), _pgLsn }, + { typeof(NpgsqlCube), _cube }, { typeof(NpgsqlPoint), _point }, { typeof(NpgsqlBox), _box }, diff --git a/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs new file mode 100644 index 000000000..7ab3076d3 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs @@ -0,0 +1,78 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class CubeQueryNpgsqlTest : IClassFixture +{ + public CubeQueryNpgqlFixture Fixture { get; } + + public CubeQueryNpgsqlTest(CubeQueryNpgqlFixture fixture) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + } + + #region Operators + + [ConditionalFact] + public void Contains_value() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Where(x => x.Cube.Contains(new NpgsqlCube(new[] { 0.0, 0.0, 0.0 }))); + var sql = result.ToQueryString(); + Assert.Equal(1, result.Single().Id); + } + + #endregion + + public class CubeQueryNpgqlFixture : SharedStoreFixtureBase + { + protected override string StoreName => "CubeQueryTest"; + protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + protected override void Seed(CubeContext context) => CubeContext.Seed(context); + } + + public class CubeContext : PoolableDbContext + { + public DbSet CubeTestEntities { get; set; } + + public CubeContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + => builder.HasPostgresExtension("cube"); + + public static void Seed(CubeContext context) + { + context.CubeTestEntities.AddRange( + new CubeTestEntity + { + Id = 1, + Cube = new NpgsqlCube(new[] { -1.0, -1.0, -1.0 }, new[] { 1.0, 1.0, 1.0 }) + }, + new CubeTestEntity + { + Id = 2, + Cube = new NpgsqlCube(new []{ 1.0, 1.0, 1.0 }) + }); + + context.SaveChanges(); + } + } + + public class CubeTestEntity + { + public int Id { get; set; } + + public NpgsqlCube Cube { get; set; } + } + + #region Helpers + + protected CubeContext CreateContext() => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #endregion +} From e130ecd144d7182142f5bfa3173f12ac7d28b092 Mon Sep 17 00:00:00 2001 From: John Gross Date: Sun, 9 Jul 2023 11:38:58 -0500 Subject: [PATCH 2/2] Add more NpgsqlCube translation --- .../NpgsqlCubeDbFunctionsExtensions.cs | 49 +++++++ .../Internal/NpgsqlCubeTranslator.cs | 129 +++++++++++++++--- .../NpgsqlMemberTranslatorProvider.cs | 1 + .../Query/CubeQueryNpgsqlTest.cs | 55 +++++++- 4 files changed, 208 insertions(+), 26 deletions(-) diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs index fcdce89a4..cabfa6e7a 100644 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs @@ -119,4 +119,53 @@ public static double DistanceTaxicab(this NpgsqlCube a, NpgsqlCube b) /// public static double DistanceChebyshev(this NpgsqlCube a, NpgsqlCube b) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev))); + + /// + /// Produces the union of two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The union of the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Union(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); + + /// + /// Produces the intersection of two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The intersection of the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Intersect(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); + + /// + /// Produces a cube enlarged by the specified radius r in at least n dimensions. If the radius is negative + /// the cube is shrunk instead. All defined dimensions are changed by the radius r. Lower-left coordinates are + /// decreased by r and upper-right coordinates are increased by r. If a lower-left coordinate is increased to more + /// than the corresponding upper-right coordinate (this can only happen when r < 0) than both coordinates are set + /// to their average. If n is greater than the number of defined dimensions and the cube is being enlarged (r > 0), + /// then extra dimensions are added to make n altogether; 0 is used as the initial value for the extra coordinates. + /// This function is useful for creating bounding boxes around a point for searching for nearby points. + /// + /// The cube to enlarge. + /// The radius by which to increase the size of the cube. + /// The number of dimensions in which to increase the size of the cube. + /// + /// A cube enlarged by the specified radius in at least the specified number of dimensions. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Enlarge(this NpgsqlCube cube, double r, int n) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Enlarge))); } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs index d0c2b1c34..6d1ccc10d 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs @@ -1,5 +1,5 @@ -using System.Security.AccessControl; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; +using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; @@ -9,7 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// 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 NpgsqlCubeTranslator : IMethodCallTranslator +public class NpgsqlCubeTranslator : IMethodCallTranslator, IMemberTranslator { private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; @@ -31,30 +31,115 @@ public NpgsqlCubeTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) IReadOnlyList arguments, IDiagnosticsLogger logger) { - if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions)) + if (method.DeclaringType == typeof(NpgsqlCubeDbFunctionsExtensions)) { - return null; + return method.Name switch + { + nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) + => _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.Contains) + => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) + => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate2) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate2, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.Distance) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.Distance, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceTaxicab, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) + => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceChebyshev, arguments[0], arguments[1]), + nameof(NpgsqlCubeDbFunctionsExtensions.Union) + => _sqlExpressionFactory.Function( + "cube_union", + new[] { arguments[0], arguments[1] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCubeDbFunctionsExtensions.Intersect) + => _sqlExpressionFactory.Function( + "cube_inter", + new[] { arguments[0], arguments[1] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCubeDbFunctionsExtensions.Enlarge) + => _sqlExpressionFactory.Function( + "cube_enlarge", + new[] { arguments[0], arguments[1], arguments[2] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType), + + _ => null + }; } - return method.Name switch + if (method.DeclaringType == typeof(NpgsqlCube) && instance != null) { - nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) - => _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.Contains) - => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) - => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) - => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate, arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate2) - => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate2, arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.Distance) - => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.Distance, arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) - => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceTaxicab, arguments[0], arguments[1]), - nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) - => _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceChebyshev, arguments[0], arguments[1]), + return method.Name switch + { + nameof(NpgsqlCube.LlCoord) + => _sqlExpressionFactory.Function( + "cube_ll_coord", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCube.UrCoord) + => _sqlExpressionFactory.Function( + "cube_ur_coord", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCube.Subset) + => _sqlExpressionFactory.Function( + "cube_subset", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + + _ => null + }; + } + + // TODO: Implement indexing into lower/upper lists with cube_ll_coord and cube_ur_coord + return null; + } + + /// + public SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType != typeof(NpgsqlCube) || instance == null) + { + return null; + } + + return member.Name switch + { + nameof(NpgsqlCube.Dimensions) + => _sqlExpressionFactory.Function( + "cube_dim", + new[] { instance }, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + returnType), + nameof(NpgsqlCube.Point) + => _sqlExpressionFactory.Function( + "cube_is_point", + new[] { instance }, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + returnType), _ => null }; } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs index ed4159dc4..926207760 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs @@ -45,6 +45,7 @@ public NpgsqlMemberTranslatorProvider( new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), new NpgsqlStringMemberTranslator(sqlExpressionFactory), new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory), + new NpgsqlCubeTranslator(sqlExpressionFactory), }); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs index 7ab3076d3..e9dae3a5a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs @@ -18,12 +18,59 @@ public CubeQueryNpgsqlTest(CubeQueryNpgqlFixture fixture) public void Contains_value() { using var context = CreateContext(); - var result = context.CubeTestEntities.Where(x => x.Cube.Contains(new NpgsqlCube(new[] { 0.0, 0.0, 0.0 }))); - var sql = result.ToQueryString(); - Assert.Equal(1, result.Single().Id); + var result = context.CubeTestEntities.Single(x => x.Cube.Contains(new NpgsqlCube(new[] { 0.0, 0.0, 0.0 }))); + Assert.Equal(1, result.Id); + AssertSql(""" + SELECT c."Id", c."Cube" + FROM "CubeTestEntities" AS c + WHERE c."Cube" @> '(0,0,0)'::cube + LIMIT 2 + """); } - #endregion + [ConditionalFact] + public void Subset() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Where(x => x.Id == 1).Select(x => x.Cube.Subset(1)).Single(); + Assert.Equal(new NpgsqlCube(-1, 1), result); + AssertSql(""" + SELECT cube_subset(c."Cube", ARRAY[1]::integer[]) + FROM "CubeTestEntities" AS c + WHERE c."Id" = 1 + LIMIT 2 + """); + } + + [ConditionalFact] + public void Dimensions() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Where(x => x.Id == 1).Select(x => x.Cube.Dimensions).Single(); + Assert.Equal(3, result); + AssertSql(""" + SELECT cube_dim(c."Cube") + FROM "CubeTestEntities" AS c + WHERE c."Id" = 1 + LIMIT 2 + """); + } + + [ConditionalFact] + public void Is_point() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Single(x => x.Cube.Point); + Assert.Equal(2, result.Id); + AssertSql(""" + SELECT c."Id", c."Cube" + FROM "CubeTestEntities" AS c + WHERE cube_is_point(c."Cube") + LIMIT 2 + """); + } + +#endregion public class CubeQueryNpgqlFixture : SharedStoreFixtureBase {