Skip to content

Commit e56c11f

Browse files
authored
Cube support (revamped) (#3651)
Closes #1938
1 parent 4c41e04 commit e56c11f

12 files changed

Lines changed: 2043 additions & 5 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<EFCoreVersion>10.0.0-rc.2.25502.107</EFCoreVersion>
44
<MicrosoftExtensionsVersion>10.0.0-rc.2.25502.107</MicrosoftExtensionsVersion>
5-
<NpgsqlVersion>10.0.0-rc.1</NpgsqlVersion>
5+
<NpgsqlVersion>10.0.0-rc.2-ci.20251107T191940</NpgsqlVersion>
66
</PropertyGroup>
77

88
<ItemGroup>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// ReSharper disable once CheckNamespace
2+
3+
using NpgsqlTypes;
4+
5+
namespace Microsoft.EntityFrameworkCore;
6+
7+
/// <summary>
8+
/// Provides extension methods for <see cref="NpgsqlCube" /> supporting PostgreSQL translation.
9+
/// </summary>
10+
/// <remarks>
11+
/// See <see href="https://www.postgresql.org/docs/current/cube.html">PostgreSQL documentation for the cube extension</see>.
12+
/// </remarks>
13+
public static class NpgsqlCubeDbFunctionsExtensions
14+
{
15+
/// <summary>
16+
/// Determines whether two cubes overlap (have points in common).
17+
/// </summary>
18+
/// <param name="cube">The first cube.</param>
19+
/// <param name="other">The second cube.</param>
20+
/// <returns>
21+
/// true if the cubes overlap; otherwise, false.
22+
/// </returns>
23+
/// <exception cref="InvalidOperationException">
24+
/// <see cref="Overlaps" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
25+
/// </exception>
26+
public static bool Overlaps(this NpgsqlCube cube, NpgsqlCube other)
27+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps)));
28+
29+
/// <summary>
30+
/// Determines whether a cube contains another cube.
31+
/// </summary>
32+
/// <param name="cube">The cube to check.</param>
33+
/// <param name="other">The cube that may be contained.</param>
34+
/// <returns>
35+
/// true if <paramref name="cube" /> contains <paramref name="other" />; otherwise, false.
36+
/// </returns>
37+
/// <exception cref="InvalidOperationException">
38+
/// <see cref="Contains" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
39+
/// </exception>
40+
public static bool Contains(this NpgsqlCube cube, NpgsqlCube other)
41+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains)));
42+
43+
/// <summary>
44+
/// Determines whether a cube is contained by another cube.
45+
/// </summary>
46+
/// <param name="cube">The cube to check.</param>
47+
/// <param name="other">The cube that may contain it.</param>
48+
/// <returns>
49+
/// true if <paramref name="cube" /> is contained by <paramref name="other" />; otherwise, false.
50+
/// </returns>
51+
/// <exception cref="InvalidOperationException">
52+
/// <see cref="ContainedBy" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
53+
/// </exception>
54+
public static bool ContainedBy(this NpgsqlCube cube, NpgsqlCube other)
55+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy)));
56+
57+
/// <summary>
58+
/// Extracts the n-th coordinate of the cube.
59+
/// </summary>
60+
/// <param name="cube">The cube.</param>
61+
/// <param name="index">The coordinate index to extract.</param>
62+
/// <returns>The coordinate value at the specified index.</returns>
63+
/// <remarks>
64+
/// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing.
65+
/// </remarks>
66+
/// <exception cref="InvalidOperationException">
67+
/// <see cref="NthCoordinate" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
68+
/// </exception>
69+
public static double NthCoordinate(this NpgsqlCube cube, int index)
70+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate)));
71+
72+
/// <summary>
73+
/// Extracts the n-th coordinate of the cube for K-nearest neighbor (KNN) indexing.
74+
/// </summary>
75+
/// <param name="cube">The cube.</param>
76+
/// <param name="index">The coordinate index to extract.</param>
77+
/// <returns>The coordinate value at the specified index.</returns>
78+
/// <remarks>
79+
/// <para>
80+
/// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing.
81+
/// </para>
82+
/// <para>
83+
/// This is the same as <see cref="NthCoordinate" /> except it is marked "lossy" for GiST indexing purposes,
84+
/// which is useful for K-nearest neighbor queries.
85+
/// </para>
86+
/// </remarks>
87+
/// <exception cref="InvalidOperationException">
88+
/// <see cref="NthCoordinateKnn" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
89+
/// </exception>
90+
public static double NthCoordinateKnn(this NpgsqlCube cube, int index)
91+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinateKnn)));
92+
93+
/// <summary>
94+
/// Computes the Euclidean distance between two cubes.
95+
/// </summary>
96+
/// <param name="cube">The first cube.</param>
97+
/// <param name="other">The second cube.</param>
98+
/// <returns>The Euclidean distance between the two cubes.</returns>
99+
/// <exception cref="InvalidOperationException">
100+
/// <see cref="Distance" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
101+
/// </exception>
102+
public static double Distance(this NpgsqlCube cube, NpgsqlCube other)
103+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance)));
104+
105+
/// <summary>
106+
/// Computes the taxicab (L-1 metric) distance between two cubes.
107+
/// </summary>
108+
/// <param name="cube">The first cube.</param>
109+
/// <param name="other">The second cube.</param>
110+
/// <returns>The taxicab distance between the two cubes.</returns>
111+
/// <exception cref="InvalidOperationException">
112+
/// <see cref="DistanceTaxicab" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
113+
/// </exception>
114+
public static double DistanceTaxicab(this NpgsqlCube cube, NpgsqlCube other)
115+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab)));
116+
117+
/// <summary>
118+
/// Computes the Chebyshev (L-inf metric) distance between two cubes.
119+
/// </summary>
120+
/// <param name="cube">The first cube.</param>
121+
/// <param name="other">The second cube.</param>
122+
/// <returns>The Chebyshev distance between the two cubes.</returns>
123+
/// <exception cref="InvalidOperationException">
124+
/// <see cref="DistanceChebyshev" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
125+
/// </exception>
126+
public static double DistanceChebyshev(this NpgsqlCube cube, NpgsqlCube other)
127+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev)));
128+
129+
/// <summary>
130+
/// Computes the union of two cubes, producing the smallest cube that encloses both.
131+
/// </summary>
132+
/// <param name="cube">The first cube.</param>
133+
/// <param name="other">The second cube.</param>
134+
/// <returns>The smallest cube that encloses both input cubes.</returns>
135+
/// <exception cref="InvalidOperationException">
136+
/// <see cref="Union" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
137+
/// </exception>
138+
public static NpgsqlCube Union(this NpgsqlCube cube, NpgsqlCube other)
139+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union)));
140+
141+
/// <summary>
142+
/// Computes the intersection of two cubes.
143+
/// </summary>
144+
/// <param name="cube">The first cube.</param>
145+
/// <param name="other">The second cube.</param>
146+
/// <returns>The intersection of the two cubes.</returns>
147+
/// <exception cref="InvalidOperationException">
148+
/// <see cref="Intersect" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
149+
/// </exception>
150+
public static NpgsqlCube Intersect(this NpgsqlCube cube, NpgsqlCube other)
151+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect)));
152+
153+
/// <summary>
154+
/// Increases the size of a cube by a specified radius in at least the specified number of dimensions.
155+
/// </summary>
156+
/// <param name="cube">The cube to enlarge.</param>
157+
/// <param name="radius">The amount by which to enlarge the cube (can be negative to shrink).</param>
158+
/// <param name="dimensions">The number of dimensions to enlarge (optional, defaults to all dimensions).</param>
159+
/// <returns>The enlarged (or shrunk) cube.</returns>
160+
/// <remarks>
161+
/// If the specified number of dimensions is greater than the cube's current dimensions,
162+
/// the extra dimensions are added with the specified radius.
163+
/// </remarks>
164+
/// <exception cref="InvalidOperationException">
165+
/// <see cref="Enlarge" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
166+
/// </exception>
167+
public static NpgsqlCube Enlarge(this NpgsqlCube cube, double radius, int dimensions)
168+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Enlarge)));
169+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
3+
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
4+
5+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
6+
7+
/// <summary>
8+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
9+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
10+
/// any release. You should only use it directly in your code with extreme caution and knowing that
11+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
12+
/// </summary>
13+
public class NpgsqlCubeTranslator(
14+
NpgsqlSqlExpressionFactory sqlExpressionFactory,
15+
IRelationalTypeMappingSource typeMappingSource) : IMethodCallTranslator, IMemberTranslator
16+
{
17+
private readonly RelationalTypeMapping _cubeTypeMapping = typeMappingSource.FindMapping(typeof(NpgsqlCube))!;
18+
private readonly RelationalTypeMapping _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!;
19+
20+
/// <inheritdoc />
21+
public virtual SqlExpression? Translate(
22+
SqlExpression? instance,
23+
MethodInfo method,
24+
IReadOnlyList<SqlExpression> arguments,
25+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
26+
{
27+
// Handle NpgsqlCubeDbFunctionsExtensions methods
28+
if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions))
29+
{
30+
return null;
31+
}
32+
33+
return method.Name switch
34+
{
35+
nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) when arguments is [var cube1, var cube2]
36+
=> sqlExpressionFactory.Overlaps(cube1, cube2),
37+
38+
nameof(NpgsqlCubeDbFunctionsExtensions.Contains) when arguments is [var cube1, var cube2]
39+
=> sqlExpressionFactory.Contains(cube1, cube2),
40+
41+
nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) when arguments is [var cube1, var cube2]
42+
=> sqlExpressionFactory.ContainedBy(cube1, cube2),
43+
44+
nameof(NpgsqlCubeDbFunctionsExtensions.Distance) when arguments is [var cube1, var cube2]
45+
=> new PgBinaryExpression(
46+
PgExpressionType.Distance,
47+
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
48+
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
49+
typeof(double),
50+
_doubleTypeMapping),
51+
52+
nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) when arguments is [var cube1, var cube2]
53+
=> new PgBinaryExpression(
54+
PgExpressionType.CubeDistanceTaxicab,
55+
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
56+
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
57+
typeof(double),
58+
_doubleTypeMapping),
59+
60+
nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) when arguments is [var cube1, var cube2]
61+
=> new PgBinaryExpression(
62+
PgExpressionType.CubeDistanceChebyshev,
63+
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
64+
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
65+
typeof(double),
66+
_doubleTypeMapping),
67+
68+
nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) when arguments is [var cube, var index]
69+
=> new PgBinaryExpression(
70+
PgExpressionType.CubeNthCoordinate,
71+
sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping),
72+
ConvertToPostgresIndex(index),
73+
typeof(double),
74+
_doubleTypeMapping),
75+
76+
nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinateKnn) when arguments is [var cube, var index]
77+
=> new PgBinaryExpression(
78+
PgExpressionType.CubeNthCoordinateKnn,
79+
sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping),
80+
ConvertToPostgresIndex(index),
81+
typeof(double),
82+
_doubleTypeMapping),
83+
84+
nameof(NpgsqlCubeDbFunctionsExtensions.Union) when arguments is [var cube1, var cube2]
85+
=> sqlExpressionFactory.Function(
86+
"cube_union",
87+
[cube1, cube2],
88+
nullable: true,
89+
argumentsPropagateNullability: TrueArrays[2],
90+
typeof(NpgsqlCube),
91+
typeMappingSource.FindMapping(typeof(NpgsqlCube))),
92+
93+
nameof(NpgsqlCubeDbFunctionsExtensions.Intersect) when arguments is [var cube1, var cube2]
94+
=> sqlExpressionFactory.Function(
95+
"cube_inter",
96+
[cube1, cube2],
97+
nullable: true,
98+
argumentsPropagateNullability: TrueArrays[2],
99+
typeof(NpgsqlCube),
100+
typeMappingSource.FindMapping(typeof(NpgsqlCube))),
101+
102+
nameof(NpgsqlCubeDbFunctionsExtensions.Enlarge) when arguments is [var cube1, var cube2, var dimension]
103+
=> sqlExpressionFactory.Function(
104+
"cube_enlarge",
105+
[cube1, cube2, dimension],
106+
nullable: true,
107+
argumentsPropagateNullability: TrueArrays[3],
108+
typeof(NpgsqlCube),
109+
typeMappingSource.FindMapping(typeof(NpgsqlCube))),
110+
111+
_ => null
112+
};
113+
}
114+
115+
/// <inheritdoc />
116+
public virtual SqlExpression? Translate(
117+
SqlExpression? instance,
118+
MemberInfo member,
119+
Type returnType,
120+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
121+
{
122+
if (member.DeclaringType != typeof(NpgsqlCube))
123+
{
124+
return null;
125+
}
126+
127+
return member.Name switch
128+
{
129+
nameof(NpgsqlCube.Dimensions)
130+
=> sqlExpressionFactory.Function(
131+
"cube_dim",
132+
[instance!],
133+
nullable: true,
134+
argumentsPropagateNullability: TrueArrays[1],
135+
typeof(int)),
136+
137+
nameof(NpgsqlCube.IsPoint)
138+
=> sqlExpressionFactory.Function(
139+
"cube_is_point",
140+
[instance!],
141+
nullable: true,
142+
argumentsPropagateNullability: TrueArrays[1],
143+
typeof(bool)),
144+
145+
nameof(NpgsqlCube.LowerLeft)
146+
=> throw new InvalidOperationException(
147+
$"The '{nameof(NpgsqlCube.LowerLeft)}' property cannot be translated to SQL. " +
148+
$"To access individual lower-left coordinates in queries, use indexer syntax (e.g., cube.LowerLeft[index]) instead."),
149+
150+
nameof(NpgsqlCube.UpperRight)
151+
=> throw new InvalidOperationException(
152+
$"The '{nameof(NpgsqlCube.UpperRight)}' property cannot be translated to SQL. " +
153+
$"To access individual upper-right coordinates in queries, use indexer syntax (e.g., cube.UpperRight[index]) instead."),
154+
155+
_ => null
156+
};
157+
}
158+
159+
/// <summary>
160+
/// Converts a zero-based index to one-based for PostgreSQL cube functions.
161+
/// For constant indexes, simplifies at translation time to avoid unnecessary addition in SQL.
162+
/// </summary>
163+
private SqlExpression ConvertToPostgresIndex(SqlExpression indexExpression)
164+
{
165+
var intTypeMapping = typeMappingSource.FindMapping(typeof(int));
166+
167+
return indexExpression is SqlConstantExpression { Value: int index }
168+
? sqlExpressionFactory.Constant(index + 1, intTypeMapping)
169+
: sqlExpressionFactory.Add(indexExpression, sqlExpressionFactory.Constant(1, intTypeMapping));
170+
}
171+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
4343
JsonPocoTranslator,
4444
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
4545
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
46-
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
46+
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
47+
new NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource)
4748
]);
4849
}
4950
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public NpgsqlMethodCallTranslatorProvider(
6262
new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount),
6363
new NpgsqlRowValueTranslator(sqlExpressionFactory),
6464
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
65-
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
65+
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
66+
new NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource),
6667
]);
6768
}
6869
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,28 @@ public enum PgExpressionType
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.
167+
/// </summary>
168+
CubeNthCoordinate, // ->
169+
170+
/// <summary>
171+
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube for KNN indexing.
172+
/// </summary>
173+
CubeNthCoordinateKnn, // ~>
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 Cube
162186
}

0 commit comments

Comments
 (0)