Skip to content

Commit

Permalink
Initial implementation of cube support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jimmacle committed Jul 9, 2023
1 parent 1d6d841 commit e854129
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Provides extension methods for <see cref="NpgsqlCube"/> supporting PostgreSQL translation.
/// </summary>
public static class NpgsqlCubeDbFunctionsExtensions
{
/// <summary>
/// Determines whether a cube overlaps with a specified cube.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>
/// <value>true</value> if the cubes overlap; otherwise, <value>false</value>.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="Overlaps" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static bool Overlaps(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps)));

/// <summary>
/// Determines whether a cube contains a specified cube.
/// </summary>
/// <param name="a">The cube in which to locate the specified cube.</param>
/// <param name="b">The specified cube to locate in the cube.</param>
/// <returns>
/// <value>true</value> if the cube contains the specified cube; otherwise, <value>false</value>.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="Contains" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static bool Contains(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains)));

/// <summary>
/// Determines whether a cube is contained by a specified cube.
/// </summary>
/// <param name="a">The cube to locate in the specified cube.</param>
/// <param name="b">The specified cube in which to locate the cube.</param>
/// <returns>
/// <value>true</value> if the cube is contained by the specified cube; otherwise, <value>false</value>.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="ContainedBy" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static bool ContainedBy(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy)));

/// <summary>
/// Extracts the n-th coordinate of the cube (counting from 1).
/// </summary>
/// <param name="cube">The cube from which to extract the specified coordinate.</param>
/// <param name="n">The specified coordinate to extract from the cube.</param>
/// <returns>
/// The n-th coordinate of the cube (counting from 1).
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="NthCoordinate" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double NthCoordinate(this NpgsqlCube cube, int n)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate)));

/// <summary>
/// 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.
/// </summary>
/// <param name="cube">The cube from which to extract the specified coordinate.</param>
/// <param name="n">The specified coordinate to extract from the cube.</param>
/// <returns>
/// The n-th coordinate of the cube, counting in the following way: n = 2 * k - 1.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="NthCoordinate2" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double NthCoordinate2(this NpgsqlCube cube, int n)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate2)));

/// <summary>
/// Computes the Euclidean distance between two cubes.
/// </summary>
/// <param name="a">The first cube.</param>
/// <param name="b">The second cube.</param>
/// <returns>
/// The Euclidean distance between the specified cubes.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="Distance" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double Distance(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance)));

/// <summary>
/// Computes the taxicab (L-1 metric) distance between two cubes.
/// </summary>
/// <param name="a">The first cube.</param>
/// <param name="b">The second cube.</param>
/// <returns>
/// The taxicab (L-1 metric) distance between the two cubes.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="DistanceTaxicab" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double DistanceTaxicab(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab)));

/// <summary>
/// Computes the Chebyshev (L-inf metric) distance between two cubes.
/// </summary>
/// <param name="a">The first cube.</param>
/// <param name="b">The second cube.</param>
/// <returns>
/// The Chebyshev (L-inf metric) distance between the two cubes.
/// </returns>
/// <exception cref="NotSupportedException">
/// <see cref="DistanceChebyshev" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double DistanceChebyshev(this NpgsqlCube a, NpgsqlCube b)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Security.AccessControl;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlCubeTranslator : IMethodCallTranslator
{
private readonly NpgsqlSqlExpressionFactory _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 NpgsqlCubeTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

/// <inheritdoc />
public SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> 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
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlCubeTranslator(sqlExpressionFactory),
});
}
}
24 changes: 24 additions & 0 deletions src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,28 @@ public enum PostgresExpressionType
LTreeFirstMatches, // ?~ or ?@

#endregion LTree

#region Cube

/// <summary>
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (counting from 1).
/// </summary>
CubeNthCoordinate, // ->

/// <summary>
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (by n = 2 * k - 1).
/// </summary>
CubeNthCoordinate2, // ~>

/// <summary>
/// Represents a PostgreSQL operator for computing the taxicab (L-1 metric) distance between two cubes.
/// </summary>
CubeDistanceTaxicab, // <#>

/// <summary>
/// Represents a PostgreSQL operator for computing the Chebyshev (L-inf metric) distance between two cubes.
/// </summary>
CubeDistanceChebyshev, // <=>

#endregion
}
5 changes: 5 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ");
Expand Down
52 changes: 52 additions & 0 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

/// <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 NpgsqlCubeTypeMapping : NpgsqlTypeMapping
{
/// <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 NpgsqlCubeTypeMapping() : base("cube", typeof(NpgsqlCube), NpgsqlDbType.Cube) {}

/// <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>
protected NpgsqlCubeTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Cube) {}

/// <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>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlCubeTypeMapping(parameters);

/// <summary>
/// Generates the SQL representation of a non-null literal value.
/// </summary>
/// <param name="value">The literal value.</param>
/// <returns>The generated string.</returns>
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";
}
}
3 changes: 3 additions & 0 deletions src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ static NpgsqlTypeMappingSource()
private readonly NpgsqlHstoreTypeMapping _immutableHstore = new(typeof(ImmutableDictionary<string, string>));
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);
Expand Down Expand Up @@ -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 } },
Expand Down Expand Up @@ -397,6 +399,7 @@ public NpgsqlTypeMappingSource(
{ typeof(Dictionary<string, string>), _hstore },
{ typeof(NpgsqlTid), _tid },
{ typeof(NpgsqlLogSequenceNumber), _pgLsn },
{ typeof(NpgsqlCube), _cube },

{ typeof(NpgsqlPoint), _point },
{ typeof(NpgsqlBox), _box },
Expand Down
78 changes: 78 additions & 0 deletions test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;

public class CubeQueryNpgsqlTest : IClassFixture<CubeQueryNpgsqlTest.CubeQueryNpgqlFixture>
{
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<CubeContext>
{
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<CubeTestEntity> 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
}

0 comments on commit e854129

Please sign in to comment.