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 +}