Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e025414
Initial EFCore.PG support for NpgsqlCube type and methods
kirkbrauer Nov 4, 2025
4c10fe8
Handle NpgsqlCube constructors via NewExpression and add more constru…
kirkbrauer Nov 5, 2025
e82a55b
Fix type mapping issues
kirkbrauer Nov 5, 2025
05ccf53
Remove unnecessary test case for Overlaps
kirkbrauer Nov 5, 2025
399b9e3
Add PgIndexes type and PgIndexesArrayExpression to represent an array…
kirkbrauer Nov 5, 2025
bef7a53
Implement LowerLeft and UpperRight indexing translation, remove PgInd…
kirkbrauer Nov 6, 2025
9c375a4
Switch to use cube.ToString() method for SQL literal
kirkbrauer Nov 7, 2025
cd97729
Fix nits and improve array translation
kirkbrauer Nov 7, 2025
f083412
Fix additional nits
kirkbrauer Nov 7, 2025
ef29c19
Use existing ToSubset() method and fix nits
kirkbrauer Nov 7, 2025
ff8756f
Set minimum PostgreSQL version for cube tests to 14
kirkbrauer Nov 7, 2025
239cee9
Re-add NpgsqlCube cast to cube GenerateNonNullSqlLiteral
kirkbrauer Nov 7, 2025
f0aa374
Refactor translation to construct SelectExpression directly and impro…
kirkbrauer Nov 7, 2025
a44e6a2
Move translation of sub-query into the expression visitor
kirkbrauer Nov 7, 2025
e9e90c2
Move all ToSubset() translation to a method in the expression visitor…
kirkbrauer Nov 7, 2025
fe6a6f2
Removed unused using statement
kirkbrauer Nov 7, 2025
413706f
Bump Npgsql CI version and fix cube distance operator conflict
kirkbrauer Nov 7, 2025
982d325
Remove unused using statement
kirkbrauer Nov 7, 2025
8acdf7d
Fix distance operator issues and remove redundant type
kirkbrauer Nov 7, 2025
62184f6
Remove cube-specific expression mapping from NpgsqlSqlExpressionFactory
kirkbrauer Nov 9, 2025
00eb348
Rename TranslateToSubset to TranslateCubeToSubset
kirkbrauer Nov 9, 2025
35e0c94
Fix missing newline on PgExpressionType.Distance revert
kirkbrauer Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<EFCoreVersion>10.0.0-rc.2.25502.107</EFCoreVersion>
<MicrosoftExtensionsVersion>10.0.0-rc.2.25502.107</MicrosoftExtensionsVersion>
<NpgsqlVersion>10.0.0-rc.1</NpgsqlVersion>
<NpgsqlVersion>10.0.0-rc.2-ci.20251107T191940</NpgsqlVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// ReSharper disable once CheckNamespace

using NpgsqlTypes;

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Provides extension methods for <see cref="NpgsqlCube" /> supporting PostgreSQL translation.
/// </summary>
/// <remarks>
/// See <see href="https://www.postgresql.org/docs/current/cube.html">PostgreSQL documentation for the cube extension</see>.
/// </remarks>
public static class NpgsqlCubeDbFunctionsExtensions
{
/// <summary>
/// Determines whether two cubes overlap (have points in common).
/// </summary>
/// <param name="cube">The first cube.</param>
/// <param name="other">The second cube.</param>
/// <returns>
/// true if the cubes overlap; otherwise, false.
/// </returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps)));

/// <summary>
/// Determines whether a cube contains another cube.
/// </summary>
/// <param name="cube">The cube to check.</param>
/// <param name="other">The cube that may be contained.</param>
/// <returns>
/// true if <paramref name="cube" /> contains <paramref name="other" />; otherwise, false.
/// </returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains)));

/// <summary>
/// Determines whether a cube is contained by another cube.
/// </summary>
/// <param name="cube">The cube to check.</param>
/// <param name="other">The cube that may contain it.</param>
/// <returns>
/// true if <paramref name="cube" /> is contained by <paramref name="other" />; otherwise, false.
/// </returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy)));

/// <summary>
/// Extracts the n-th coordinate of the cube.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="index">The coordinate index to extract.</param>
/// <returns>The coordinate value at the specified index.</returns>
/// <remarks>
/// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// <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 index)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate)));

/// <summary>
/// Extracts the n-th coordinate of the cube for K-nearest neighbor (KNN) indexing.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="index">The coordinate index to extract.</param>
/// <returns>The coordinate value at the specified index.</returns>
/// <remarks>
/// <para>
/// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing.
/// </para>
/// <para>
/// This is the same as <see cref="NthCoordinate" /> except it is marked "lossy" for GiST indexing purposes,
/// which is useful for K-nearest neighbor queries.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// <see cref="NthCoordinateKnn" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static double NthCoordinateKnn(this NpgsqlCube cube, int index)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinateKnn)));

/// <summary>
/// Computes the Euclidean distance between two cubes.
/// </summary>
/// <param name="cube">The first cube.</param>
/// <param name="other">The second cube.</param>
/// <returns>The Euclidean distance between the two cubes.</returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance)));

/// <summary>
/// Computes the taxicab (L-1 metric) distance between two cubes.
/// </summary>
/// <param name="cube">The first cube.</param>
/// <param name="other">The second cube.</param>
/// <returns>The taxicab distance between the two cubes.</returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab)));

/// <summary>
/// Computes the Chebyshev (L-inf metric) distance between two cubes.
/// </summary>
/// <param name="cube">The first cube.</param>
/// <param name="other">The second cube.</param>
/// <returns>The Chebyshev distance between the two cubes.</returns>
/// <exception cref="InvalidOperationException">
/// <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 cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev)));

/// <summary>
/// Computes the union of two cubes, producing the smallest cube that encloses both.
/// </summary>
/// <param name="cube">The first cube.</param>
/// <param name="other">The second cube.</param>
/// <returns>The smallest cube that encloses both input cubes.</returns>
/// <exception cref="InvalidOperationException">
/// <see cref="Union" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static NpgsqlCube Union(this NpgsqlCube cube, NpgsqlCube other)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union)));

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

/// <summary>
/// Increases the size of a cube by a specified radius in at least the specified number of dimensions.
/// </summary>
/// <param name="cube">The cube to enlarge.</param>
/// <param name="radius">The amount by which to enlarge the cube (can be negative to shrink).</param>
/// <param name="dimensions">The number of dimensions to enlarge (optional, defaults to all dimensions).</param>
/// <returns>The enlarged (or shrunk) cube.</returns>
/// <remarks>
/// If the specified number of dimensions is greater than the cube's current dimensions,
/// the extra dimensions are added with the specified radius.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// <see cref="Enlarge" /> is only intended for use via SQL translation as part of an EF Core LINQ query.
/// </exception>
public static NpgsqlCube Enlarge(this NpgsqlCube cube, double radius, int dimensions)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Enlarge)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;

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(
NpgsqlSqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource) : IMethodCallTranslator, IMemberTranslator
{
private readonly RelationalTypeMapping _cubeTypeMapping = typeMappingSource.FindMapping(typeof(NpgsqlCube))!;
private readonly RelationalTypeMapping _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!;

/// <inheritdoc />
public virtual SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
// Handle NpgsqlCubeDbFunctionsExtensions methods
if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions))
{
return null;
}

return method.Name switch
{
nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) when arguments is [var cube1, var cube2]
=> sqlExpressionFactory.Overlaps(cube1, cube2),

nameof(NpgsqlCubeDbFunctionsExtensions.Contains) when arguments is [var cube1, var cube2]
=> sqlExpressionFactory.Contains(cube1, cube2),

nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) when arguments is [var cube1, var cube2]
=> sqlExpressionFactory.ContainedBy(cube1, cube2),

nameof(NpgsqlCubeDbFunctionsExtensions.Distance) when arguments is [var cube1, var cube2]
=> new PgBinaryExpression(
PgExpressionType.Distance,
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
typeof(double),
_doubleTypeMapping),

nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) when arguments is [var cube1, var cube2]
=> new PgBinaryExpression(
PgExpressionType.CubeDistanceTaxicab,
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
typeof(double),
_doubleTypeMapping),

nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) when arguments is [var cube1, var cube2]
=> new PgBinaryExpression(
PgExpressionType.CubeDistanceChebyshev,
sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping),
sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping),
typeof(double),
_doubleTypeMapping),

nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) when arguments is [var cube, var index]
=> new PgBinaryExpression(
PgExpressionType.CubeNthCoordinate,
sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping),
ConvertToPostgresIndex(index),
typeof(double),
_doubleTypeMapping),

nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinateKnn) when arguments is [var cube, var index]
=> new PgBinaryExpression(
PgExpressionType.CubeNthCoordinateKnn,
sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping),
ConvertToPostgresIndex(index),
typeof(double),
_doubleTypeMapping),

nameof(NpgsqlCubeDbFunctionsExtensions.Union) when arguments is [var cube1, var cube2]
=> sqlExpressionFactory.Function(
"cube_union",
[cube1, cube2],
nullable: true,
argumentsPropagateNullability: TrueArrays[2],
typeof(NpgsqlCube),
typeMappingSource.FindMapping(typeof(NpgsqlCube))),

nameof(NpgsqlCubeDbFunctionsExtensions.Intersect) when arguments is [var cube1, var cube2]
=> sqlExpressionFactory.Function(
"cube_inter",
[cube1, cube2],
nullable: true,
argumentsPropagateNullability: TrueArrays[2],
typeof(NpgsqlCube),
typeMappingSource.FindMapping(typeof(NpgsqlCube))),

nameof(NpgsqlCubeDbFunctionsExtensions.Enlarge) when arguments is [var cube1, var cube2, var dimension]
=> sqlExpressionFactory.Function(
"cube_enlarge",
[cube1, cube2, dimension],
nullable: true,
argumentsPropagateNullability: TrueArrays[3],
typeof(NpgsqlCube),
typeMappingSource.FindMapping(typeof(NpgsqlCube))),

_ => null
};
}

/// <inheritdoc />
public virtual SqlExpression? Translate(
SqlExpression? instance,
MemberInfo member,
Type returnType,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (member.DeclaringType != typeof(NpgsqlCube))
{
return null;
}

return member.Name switch
{
nameof(NpgsqlCube.Dimensions)
=> sqlExpressionFactory.Function(
"cube_dim",
[instance!],
nullable: true,
argumentsPropagateNullability: TrueArrays[1],
typeof(int)),

nameof(NpgsqlCube.IsPoint)
=> sqlExpressionFactory.Function(
"cube_is_point",
[instance!],
nullable: true,
argumentsPropagateNullability: TrueArrays[1],
typeof(bool)),

nameof(NpgsqlCube.LowerLeft)
=> throw new InvalidOperationException(
$"The '{nameof(NpgsqlCube.LowerLeft)}' property cannot be translated to SQL. " +
$"To access individual lower-left coordinates in queries, use indexer syntax (e.g., cube.LowerLeft[index]) instead."),

nameof(NpgsqlCube.UpperRight)
=> throw new InvalidOperationException(
$"The '{nameof(NpgsqlCube.UpperRight)}' property cannot be translated to SQL. " +
$"To access individual upper-right coordinates in queries, use indexer syntax (e.g., cube.UpperRight[index]) instead."),

_ => null
};
}

/// <summary>
/// Converts a zero-based index to one-based for PostgreSQL cube functions.
/// For constant indexes, simplifies at translation time to avoid unnecessary addition in SQL.
/// </summary>
private SqlExpression ConvertToPostgresIndex(SqlExpression indexExpression)
{
var intTypeMapping = typeMappingSource.FindMapping(typeof(int));

return indexExpression is SqlConstantExpression { Value: int index }
? sqlExpressionFactory.Constant(index + 1, intTypeMapping)
: sqlExpressionFactory.Add(indexExpression, sqlExpressionFactory.Constant(1, intTypeMapping));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
new NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount),
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource),
]);
}
}
24 changes: 24 additions & 0 deletions src/EFCore.PG/Query/Expressions/PgExpressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,28 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@

#endregion LTree

#region Cube

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

/// <summary>
/// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube for KNN indexing.
/// </summary>
CubeNthCoordinateKnn, // ~>

/// <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 Cube
}
Loading