Skip to content

Commit e854129

Browse files
committed
Initial implementation of cube support
1 parent 1d6d841 commit e854129

File tree

8 files changed

+346
-0
lines changed

8 files changed

+346
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// ReSharper disable once CheckNamespace
2+
namespace Microsoft.EntityFrameworkCore;
3+
4+
/// <summary>
5+
/// Provides extension methods for <see cref="NpgsqlCube"/> supporting PostgreSQL translation.
6+
/// </summary>
7+
public static class NpgsqlCubeDbFunctionsExtensions
8+
{
9+
/// <summary>
10+
/// Determines whether a cube overlaps with a specified cube.
11+
/// </summary>
12+
/// <param name="a"></param>
13+
/// <param name="b"></param>
14+
/// <returns>
15+
/// <value>true</value> if the cubes overlap; otherwise, <value>false</value>.
16+
/// </returns>
17+
/// <exception cref="NotSupportedException">
18+
/// <see cref="Overlaps" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
19+
/// </exception>
20+
public static bool Overlaps(this NpgsqlCube a, NpgsqlCube b)
21+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps)));
22+
23+
/// <summary>
24+
/// Determines whether a cube contains a specified cube.
25+
/// </summary>
26+
/// <param name="a">The cube in which to locate the specified cube.</param>
27+
/// <param name="b">The specified cube to locate in the cube.</param>
28+
/// <returns>
29+
/// <value>true</value> if the cube contains the specified cube; otherwise, <value>false</value>.
30+
/// </returns>
31+
/// <exception cref="NotSupportedException">
32+
/// <see cref="Contains" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
33+
/// </exception>
34+
public static bool Contains(this NpgsqlCube a, NpgsqlCube b)
35+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains)));
36+
37+
/// <summary>
38+
/// Determines whether a cube is contained by a specified cube.
39+
/// </summary>
40+
/// <param name="a">The cube to locate in the specified cube.</param>
41+
/// <param name="b">The specified cube in which to locate the cube.</param>
42+
/// <returns>
43+
/// <value>true</value> if the cube is contained by the specified cube; otherwise, <value>false</value>.
44+
/// </returns>
45+
/// <exception cref="NotSupportedException">
46+
/// <see cref="ContainedBy" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
47+
/// </exception>
48+
public static bool ContainedBy(this NpgsqlCube a, NpgsqlCube b)
49+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy)));
50+
51+
/// <summary>
52+
/// Extracts the n-th coordinate of the cube (counting from 1).
53+
/// </summary>
54+
/// <param name="cube">The cube from which to extract the specified coordinate.</param>
55+
/// <param name="n">The specified coordinate to extract from the cube.</param>
56+
/// <returns>
57+
/// The n-th coordinate of the cube (counting from 1).
58+
/// </returns>
59+
/// <exception cref="NotSupportedException">
60+
/// <see cref="NthCoordinate" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
61+
/// </exception>
62+
public static double NthCoordinate(this NpgsqlCube cube, int n)
63+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate)));
64+
65+
/// <summary>
66+
/// Extracts the n-th coordinate of the cube, counting in the following way: n = 2 * k - 1 means lower bound
67+
/// of k-th dimension, n = 2 * k means upper bound of k-th dimension. Negative n denotes the inverse value
68+
/// of the corresponding positive coordinate. This operator is designed for KNN-GiST support.
69+
/// </summary>
70+
/// <param name="cube">The cube from which to extract the specified coordinate.</param>
71+
/// <param name="n">The specified coordinate to extract from the cube.</param>
72+
/// <returns>
73+
/// The n-th coordinate of the cube, counting in the following way: n = 2 * k - 1.
74+
/// </returns>
75+
/// <exception cref="NotSupportedException">
76+
/// <see cref="NthCoordinate2" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
77+
/// </exception>
78+
public static double NthCoordinate2(this NpgsqlCube cube, int n)
79+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate2)));
80+
81+
/// <summary>
82+
/// Computes the Euclidean distance between two cubes.
83+
/// </summary>
84+
/// <param name="a">The first cube.</param>
85+
/// <param name="b">The second cube.</param>
86+
/// <returns>
87+
/// The Euclidean distance between the specified cubes.
88+
/// </returns>
89+
/// <exception cref="NotSupportedException">
90+
/// <see cref="Distance" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
91+
/// </exception>
92+
public static double Distance(this NpgsqlCube a, NpgsqlCube b)
93+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance)));
94+
95+
/// <summary>
96+
/// Computes the taxicab (L-1 metric) distance between two cubes.
97+
/// </summary>
98+
/// <param name="a">The first cube.</param>
99+
/// <param name="b">The second cube.</param>
100+
/// <returns>
101+
/// The taxicab (L-1 metric) distance between the two cubes.
102+
/// </returns>
103+
/// <exception cref="NotSupportedException">
104+
/// <see cref="DistanceTaxicab" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
105+
/// </exception>
106+
public static double DistanceTaxicab(this NpgsqlCube a, NpgsqlCube b)
107+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab)));
108+
109+
/// <summary>
110+
/// Computes the Chebyshev (L-inf metric) distance between two cubes.
111+
/// </summary>
112+
/// <param name="a">The first cube.</param>
113+
/// <param name="b">The second cube.</param>
114+
/// <returns>
115+
/// The Chebyshev (L-inf metric) distance between the two cubes.
116+
/// </returns>
117+
/// <exception cref="NotSupportedException">
118+
/// <see cref="DistanceChebyshev" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
119+
/// </exception>
120+
public static double DistanceChebyshev(this NpgsqlCube a, NpgsqlCube b)
121+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev)));
122+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Security.AccessControl;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
3+
4+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public class NpgsqlCubeTranslator : IMethodCallTranslator
13+
{
14+
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
15+
16+
/// <summary>
17+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
18+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
19+
/// any release. You should only use it directly in your code with extreme caution and knowing that
20+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
21+
/// </summary>
22+
public NpgsqlCubeTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory)
23+
{
24+
_sqlExpressionFactory = sqlExpressionFactory;
25+
}
26+
27+
/// <inheritdoc />
28+
public SqlExpression? Translate(
29+
SqlExpression? instance,
30+
MethodInfo method,
31+
IReadOnlyList<SqlExpression> arguments,
32+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
33+
{
34+
if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions))
35+
{
36+
return null;
37+
}
38+
39+
return method.Name switch
40+
{
41+
nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps)
42+
=> _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]),
43+
nameof(NpgsqlCubeDbFunctionsExtensions.Contains)
44+
=> _sqlExpressionFactory.Contains(arguments[0], arguments[1]),
45+
nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy)
46+
=> _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]),
47+
nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate)
48+
=> _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate, arguments[0], arguments[1]),
49+
nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate2)
50+
=> _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeNthCoordinate2, arguments[0], arguments[1]),
51+
nameof(NpgsqlCubeDbFunctionsExtensions.Distance)
52+
=> _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.Distance, arguments[0], arguments[1]),
53+
nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab)
54+
=> _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceTaxicab, arguments[0], arguments[1]),
55+
nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev)
56+
=> _sqlExpressionFactory.MakePostgresBinary(PostgresExpressionType.CubeDistanceChebyshev, arguments[0], arguments[1]),
57+
58+
_ => null
59+
};
60+
}
61+
}

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public NpgsqlMethodCallTranslatorProvider(
6363
new NpgsqlRowValueTranslator(sqlExpressionFactory),
6464
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
6565
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
66+
new NpgsqlCubeTranslator(sqlExpressionFactory),
6667
});
6768
}
6869
}

src/EFCore.PG/Query/Expressions/PostgresExpressionType.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,28 @@ public enum PostgresExpressionType
159159
LTreeFirstMatches, // ?~ or ?@
160160

161161
#endregion LTree
162+
163+
#region Cube
164+
165+
/// <summary>
166+
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (counting from 1).
167+
/// </summary>
168+
CubeNthCoordinate, // ->
169+
170+
/// <summary>
171+
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube (by n = 2 * k - 1).
172+
/// </summary>
173+
CubeNthCoordinate2, // ~>
174+
175+
/// <summary>
176+
/// Represents a PostgreSQL operator for computing the taxicab (L-1 metric) distance between two cubes.
177+
/// </summary>
178+
CubeDistanceTaxicab, // <#>
179+
180+
/// <summary>
181+
/// Represents a PostgreSQL operator for computing the Chebyshev (L-inf metric) distance between two cubes.
182+
/// </summary>
183+
CubeDistanceChebyshev, // <=>
184+
185+
#endregion
162186
}

src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,11 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
512512

513513
PostgresExpressionType.Distance => "<->",
514514

515+
PostgresExpressionType.CubeNthCoordinate => "->",
516+
PostgresExpressionType.CubeNthCoordinate2 => "~>",
517+
PostgresExpressionType.CubeDistanceTaxicab => "<#>",
518+
PostgresExpressionType.CubeDistanceChebyshev => "<=>",
519+
515520
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
516521
})
517522
.Append(" ");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
2+
3+
/// <summary>
4+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
5+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
6+
/// any release. You should only use it directly in your code with extreme caution and knowing that
7+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
8+
/// </summary>
9+
public class NpgsqlCubeTypeMapping : NpgsqlTypeMapping
10+
{
11+
/// <summary>
12+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
13+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
14+
/// any release. You should only use it directly in your code with extreme caution and knowing that
15+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
16+
/// </summary>
17+
public NpgsqlCubeTypeMapping() : base("cube", typeof(NpgsqlCube), NpgsqlDbType.Cube) {}
18+
19+
/// <summary>
20+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
21+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
22+
/// any release. You should only use it directly in your code with extreme caution and knowing that
23+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
24+
/// </summary>
25+
protected NpgsqlCubeTypeMapping(RelationalTypeMappingParameters parameters)
26+
: base(parameters, NpgsqlDbType.Cube) {}
27+
28+
/// <summary>
29+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
30+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
31+
/// any release. You should only use it directly in your code with extreme caution and knowing that
32+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
33+
/// </summary>
34+
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
35+
=> new NpgsqlCubeTypeMapping(parameters);
36+
37+
/// <summary>
38+
/// Generates the SQL representation of a non-null literal value.
39+
/// </summary>
40+
/// <param name="value">The literal value.</param>
41+
/// <returns>The generated string.</returns>
42+
protected override string GenerateNonNullSqlLiteral(object value)
43+
{
44+
if (!(value is NpgsqlCube cube))
45+
throw new InvalidOperationException($"Can't generate a cube SQL literal for CLR type {value.GetType()}");
46+
47+
if (cube.Point)
48+
return $"'({string.Join(",", cube.LowerLeft)})'::cube";
49+
else
50+
return $"'({string.Join(",", cube.LowerLeft)}),({string.Join(",", cube.UpperRight)})'::cube";
51+
}
52+
}

src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ static NpgsqlTypeMappingSource()
183183
private readonly NpgsqlHstoreTypeMapping _immutableHstore = new(typeof(ImmutableDictionary<string, string>));
184184
private readonly NpgsqlTidTypeMapping _tid = new();
185185
private readonly NpgsqlPgLsnTypeMapping _pgLsn = new();
186+
private readonly NpgsqlCubeTypeMapping _cube = new();
186187

187188
private readonly NpgsqlLTreeTypeMapping _ltree = new();
188189
private readonly NpgsqlStringTypeMapping _ltreeString = new("ltree", NpgsqlDbType.LTree);
@@ -336,6 +337,7 @@ public NpgsqlTypeMappingSource(
336337
{ "lo", new[] { _lo } },
337338
{ "tid", new[] { _tid } },
338339
{ "pg_lsn", new[] { _pgLsn } },
340+
{ "cube", new[] { _cube } },
339341

340342
{ "int4range", new[] { _int4range } },
341343
{ "int8range", new[] { _int8range } },
@@ -397,6 +399,7 @@ public NpgsqlTypeMappingSource(
397399
{ typeof(Dictionary<string, string>), _hstore },
398400
{ typeof(NpgsqlTid), _tid },
399401
{ typeof(NpgsqlLogSequenceNumber), _pgLsn },
402+
{ typeof(NpgsqlCube), _cube },
400403

401404
{ typeof(NpgsqlPoint), _point },
402405
{ typeof(NpgsqlBox), _box },
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
2+
3+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
4+
5+
public class CubeQueryNpgsqlTest : IClassFixture<CubeQueryNpgsqlTest.CubeQueryNpgqlFixture>
6+
{
7+
public CubeQueryNpgqlFixture Fixture { get; }
8+
9+
public CubeQueryNpgsqlTest(CubeQueryNpgqlFixture fixture)
10+
{
11+
Fixture = fixture;
12+
Fixture.TestSqlLoggerFactory.Clear();
13+
}
14+
15+
#region Operators
16+
17+
[ConditionalFact]
18+
public void Contains_value()
19+
{
20+
using var context = CreateContext();
21+
var result = context.CubeTestEntities.Where(x => x.Cube.Contains(new NpgsqlCube(new[] { 0.0, 0.0, 0.0 })));
22+
var sql = result.ToQueryString();
23+
Assert.Equal(1, result.Single().Id);
24+
}
25+
26+
#endregion
27+
28+
public class CubeQueryNpgqlFixture : SharedStoreFixtureBase<CubeContext>
29+
{
30+
protected override string StoreName => "CubeQueryTest";
31+
protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance;
32+
public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory;
33+
protected override void Seed(CubeContext context) => CubeContext.Seed(context);
34+
}
35+
36+
public class CubeContext : PoolableDbContext
37+
{
38+
public DbSet<CubeTestEntity> CubeTestEntities { get; set; }
39+
40+
public CubeContext(DbContextOptions options) : base(options) { }
41+
42+
protected override void OnModelCreating(ModelBuilder builder)
43+
=> builder.HasPostgresExtension("cube");
44+
45+
public static void Seed(CubeContext context)
46+
{
47+
context.CubeTestEntities.AddRange(
48+
new CubeTestEntity
49+
{
50+
Id = 1,
51+
Cube = new NpgsqlCube(new[] { -1.0, -1.0, -1.0 }, new[] { 1.0, 1.0, 1.0 })
52+
},
53+
new CubeTestEntity
54+
{
55+
Id = 2,
56+
Cube = new NpgsqlCube(new []{ 1.0, 1.0, 1.0 })
57+
});
58+
59+
context.SaveChanges();
60+
}
61+
}
62+
63+
public class CubeTestEntity
64+
{
65+
public int Id { get; set; }
66+
67+
public NpgsqlCube Cube { get; set; }
68+
}
69+
70+
#region Helpers
71+
72+
protected CubeContext CreateContext() => Fixture.CreateContext();
73+
74+
private void AssertSql(params string[] expected)
75+
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
76+
77+
#endregion
78+
}

0 commit comments

Comments
 (0)