diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs new file mode 100644 index 000000000..cabfa6e7a --- /dev/null +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs @@ -0,0 +1,171 @@ +// 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))); + + /// + /// Produces the union of two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The union of the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Union(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); + + /// + /// Produces the intersection of two cubes. + /// + /// The first cube. + /// The second cube. + /// + /// The intersection of the two cubes. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Intersect(this NpgsqlCube a, NpgsqlCube b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); + + /// + /// Produces a cube enlarged by the specified radius r in at least n dimensions. If the radius is negative + /// the cube is shrunk instead. All defined dimensions are changed by the radius r. Lower-left coordinates are + /// decreased by r and upper-right coordinates are increased by r. If a lower-left coordinate is increased to more + /// than the corresponding upper-right coordinate (this can only happen when r < 0) than both coordinates are set + /// to their average. If n is greater than the number of defined dimensions and the cube is being enlarged (r > 0), + /// then extra dimensions are added to make n altogether; 0 is used as the initial value for the extra coordinates. + /// This function is useful for creating bounding boxes around a point for searching for nearby points. + /// + /// The cube to enlarge. + /// The radius by which to increase the size of the cube. + /// The number of dimensions in which to increase the size of the cube. + /// + /// A cube enlarged by the specified radius in at least the specified number of dimensions. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static NpgsqlCube Enlarge(this NpgsqlCube cube, double r, int n) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Enlarge))); +} 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..6d1ccc10d --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs @@ -0,0 +1,146 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; +using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; + +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, IMemberTranslator +{ + 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 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]), + nameof(NpgsqlCubeDbFunctionsExtensions.Union) + => _sqlExpressionFactory.Function( + "cube_union", + new[] { arguments[0], arguments[1] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCubeDbFunctionsExtensions.Intersect) + => _sqlExpressionFactory.Function( + "cube_inter", + new[] { arguments[0], arguments[1] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCubeDbFunctionsExtensions.Enlarge) + => _sqlExpressionFactory.Function( + "cube_enlarge", + new[] { arguments[0], arguments[1], arguments[2] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType), + + _ => null + }; + } + + if (method.DeclaringType == typeof(NpgsqlCube) && instance != null) + { + return method.Name switch + { + nameof(NpgsqlCube.LlCoord) + => _sqlExpressionFactory.Function( + "cube_ll_coord", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCube.UrCoord) + => _sqlExpressionFactory.Function( + "cube_ur_coord", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + nameof(NpgsqlCube.Subset) + => _sqlExpressionFactory.Function( + "cube_subset", + new[] { instance, arguments[0] }, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + + _ => null + }; + } + + // TODO: Implement indexing into lower/upper lists with cube_ll_coord and cube_ur_coord + + return null; + } + + /// + public SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType != typeof(NpgsqlCube) || instance == null) + { + return null; + } + + return member.Name switch + { + nameof(NpgsqlCube.Dimensions) + => _sqlExpressionFactory.Function( + "cube_dim", + new[] { instance }, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + returnType), + nameof(NpgsqlCube.Point) + => _sqlExpressionFactory.Function( + "cube_is_point", + new[] { instance }, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + returnType), + _ => null + }; + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs index ed4159dc4..926207760 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs @@ -45,6 +45,7 @@ public NpgsqlMemberTranslatorProvider( new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), new NpgsqlStringMemberTranslator(sqlExpressionFactory), new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory), + new NpgsqlCubeTranslator(sqlExpressionFactory), }); } } 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..e9dae3a5a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/CubeQueryNpgsqlTest.cs @@ -0,0 +1,125 @@ +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.Single(x => x.Cube.Contains(new NpgsqlCube(new[] { 0.0, 0.0, 0.0 }))); + Assert.Equal(1, result.Id); + AssertSql(""" + SELECT c."Id", c."Cube" + FROM "CubeTestEntities" AS c + WHERE c."Cube" @> '(0,0,0)'::cube + LIMIT 2 + """); + } + + [ConditionalFact] + public void Subset() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Where(x => x.Id == 1).Select(x => x.Cube.Subset(1)).Single(); + Assert.Equal(new NpgsqlCube(-1, 1), result); + AssertSql(""" + SELECT cube_subset(c."Cube", ARRAY[1]::integer[]) + FROM "CubeTestEntities" AS c + WHERE c."Id" = 1 + LIMIT 2 + """); + } + + [ConditionalFact] + public void Dimensions() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Where(x => x.Id == 1).Select(x => x.Cube.Dimensions).Single(); + Assert.Equal(3, result); + AssertSql(""" + SELECT cube_dim(c."Cube") + FROM "CubeTestEntities" AS c + WHERE c."Id" = 1 + LIMIT 2 + """); + } + + [ConditionalFact] + public void Is_point() + { + using var context = CreateContext(); + var result = context.CubeTestEntities.Single(x => x.Cube.Point); + Assert.Equal(2, result.Id); + AssertSql(""" + SELECT c."Id", c."Cube" + FROM "CubeTestEntities" AS c + WHERE cube_is_point(c."Cube") + LIMIT 2 + """); + } + +#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 +}