diff --git a/src/EFCore.PG/NpgsqlRangeExtensions.cs b/src/EFCore.PG/NpgsqlRangeExtensions.cs new file mode 100644 index 000000000..d50b555bf --- /dev/null +++ b/src/EFCore.PG/NpgsqlRangeExtensions.cs @@ -0,0 +1,168 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL +{ + /// + /// Provides extension methods for supporting PostgreSQL translation. + /// + public static class NpgsqlRangeExtensions + { + /// + /// Determines whether a range contains a specified value. + /// + /// The range in which to locate the value. + /// The value to locate in the range. + /// The type of the elements of . + /// + /// true if the range contains the specified value; otherwise, false. + /// + public static bool Contains(this NpgsqlRange range, T value) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range contains a specified range. + /// + /// The range in which to locate the specified range. + /// The specified range to locate in the range. + /// The type of the elements of . + /// + /// true if the range contains the specified range; otherwise, false. + /// + public static bool Contains(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range is contained by a specified range. + /// + /// The specified range to locate in the range. + /// The range in which to locate the specified range. + /// The type of the elements of . + /// + /// true if the range contains the specified range; otherwise, false. + /// + public static bool ContainedBy(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => b.Contains(a); + + /// + /// Determines whether a range overlaps another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the ranges overlap (share points in common); otherwise, false. + /// + public static bool Overlaps(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range is strictly to the left of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the first range is strictly to the left of the second; otherwise, false. + /// + public static bool IsStrictlyLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range is strictly to the right of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the first range is strictly to the right of the second; otherwise, false. + /// + public static bool IsStrictlyRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range does not extend to the left of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the first range does not extend to the left of the second; otherwise, false. + /// + public static bool DoesNotExtendLeftOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range does not extend to the right of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the first range does not extend to the right of the second; otherwise, false. + /// + public static bool DoesNotExtendRightOf(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Determines whether a range is adjacent to another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true if the ranges are adjacent; otherwise, false. + /// + public static bool IsAdjacentTo(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Returns the set union, which means unique elements that appear in either of two ranges. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// The unique elements that appear in either range. + /// + public static NpgsqlRange Union(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Returns the set intersection, which means elements that appear in each of two ranges. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// The elements that appear in both ranges. + /// + public static NpgsqlRange Intersect(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + + /// + /// Returns the set difference, which means the elements of one range that do not appear in a second range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// The elements that appear in the first range, but not the second range. + /// + public static NpgsqlRange Except(this NpgsqlRange a, NpgsqlRange b) where T : IComparable => throw new NotSupportedException(); + } +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 54feb84ab..3011d4b67 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -50,7 +50,8 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall new NpgsqlStringTrimEndTranslator(), new NpgsqlStringTrimStartTranslator(), new NpgsqlRegexIsMatchTranslator(), - new NpgsqlFullTextSearchMethodTranslator() + new NpgsqlFullTextSearchMethodTranslator(), + new NpgsqlRangeTranslator() }; public NpgsqlCompositeMethodCallTranslator( diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs new file mode 100644 index 000000000..39127c444 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs @@ -0,0 +1,85 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + /// + /// Provides translation services for PostgreSQL range operators. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-range.html + /// + public class NpgsqlRangeTranslator : IMethodCallTranslator + { + /// + [CanBeNull] + public Expression Translate(MethodCallExpression expression) + { + switch (expression.Method.Name) + { + case nameof(NpgsqlRangeExtensions.Contains): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "@>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.ContainedBy): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<@", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.Overlaps): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&&", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsStrictlyLeftOf): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "<<", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsStrictlyRightOf): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], ">>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.DoesNotExtendRightOf): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&<", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.DoesNotExtendLeftOf): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "&>", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.IsAdjacentTo): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-|-", typeof(bool)); + + case nameof(NpgsqlRangeExtensions.Union): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "+", expression.Arguments[0].Type); + + case nameof(NpgsqlRangeExtensions.Intersect): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "*", expression.Arguments[0].Type); + + case nameof(NpgsqlRangeExtensions.Except): + return new CustomBinaryExpression(expression.Arguments[0], expression.Arguments[1], "-", expression.Arguments[0].Type); + + default: + return null; + } + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index 050106099..e6f2d8420 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -26,4 +26,4 @@ - + \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs new file mode 100644 index 000000000..c31d32016 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs @@ -0,0 +1,691 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using NpgsqlTypes; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + /// + /// Provides unit tests for range operator translations. + /// + public class RangeQueryNpgsqlTest : IClassFixture + { + /// + /// Provides resources for unit tests. + /// + RangeQueryNpgsqlFixture Fixture { get; } + + /// + /// Initializes resources for unit tests. + /// + /// The fixture of resources for testing. + public RangeQueryNpgsqlTest(RangeQueryNpgsqlFixture fixture) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + } + + #region Tests + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeContainsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Contains(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" @> @__range_0 = TRUE"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotContainRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Contains(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" @> @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(IntegerTheoryData))] + public void RangeContainsValue(int value) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Contains(value)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" @> @__value_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(IntegerTheoryData))] + public void RangeDoesNotContainValue(int value) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Contains(value)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" @> @__value_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeContainedByRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => range.ContainedBy(x.Range)) + .ToArray(); + + AssertContainsSql("WHERE @__range_0 <@ x.\"Range\" = TRUE"); + } + } + + /// + /// Tests inverse containment translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeNotContainedByRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !range.ContainedBy(x.Range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (@__range_0 <@ x.\"Range\" = TRUE)"); + } + } + + /// + /// Tests translation for via the symbolic operator. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeEqualsRange_Operator(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range == range) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" = @__range_0"); + } + } + + /// + /// Tests translation for via method call. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeEqualsRange_Method(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Equals(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" = @__range_0"); + } + } + + /// + /// Tests inverse translation for via symbolic operator. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotEqualsRange_Operator(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range != range) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" <> @__range_0"); + } + } + + /// + /// Tests inverse translation for via method call. + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotEqualsRange_Method(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Equals(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" <> @__range_0"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeOvelapsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.Overlaps(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" && @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotOvelapRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.Overlaps(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" && @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsStrictlyLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsStrictlyLeftOf(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" << @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotStrictlyLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsStrictlyLeftOf(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" << @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsStrictlyRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsStrictlyRightOf(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" >> @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotStrictlyRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsStrictlyRightOf(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" >> @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotExtendLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.DoesNotExtendLeftOf(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" &> @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesExtendLeftOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.DoesNotExtendLeftOf(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" &> @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesNotExtendRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.DoesNotExtendRightOf(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" &< @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeDoesExtendRightOfRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.DoesNotExtendRightOf(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" &< @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsAdjacentToRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => x.Range.IsAdjacentTo(range)) + .ToArray(); + + AssertContainsSql("WHERE x.\"Range\" -|- @__range_0"); + } + } + + /// + /// Tests inverse translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIsNotAdjacentToRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + RangeTestEntity[] _ = + context.RangeTestEntities + .Where(x => !x.Range.IsAdjacentTo(range)) + .ToArray(); + + AssertContainsSql("WHERE NOT (x.\"Range\" -|- @__range_0 = TRUE)"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeUnionRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Union(range)) + .ToArray(); + + AssertContainsSql("SELECT x.\"Range\" + @__range_0"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeIntersectsRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Intersect(range)) + .ToArray(); + + AssertContainsSql("SELECT x.\"Range\" * @__range_0"); + } + } + + /// + /// Tests translation for . + /// + [Theory] + [MemberData(nameof(RangeTheoryData))] + public void RangeExceptRange(NpgsqlRange range) + { + using (RangeContext context = Fixture.CreateContext()) + { + try + { + NpgsqlRange[] _ = + context.RangeTestEntities + .Select(x => x.Range.Except(range)) + .ToArray(); + } + catch (PostgresException) + { + // ignore: Npgsql.PostgresException : 22000: result of range difference would not be contiguous. + } + + AssertContainsSql("SELECT x.\"Range\" - @__range_0"); + } + } + + #endregion Tests + + #region TheoryData + + /// + /// Provides theory data for integers. + /// + public static IEnumerable IntegerTheoryData => Enumerable.Range(-10, 10).Select(x => new object[] { x }); + + /// + /// Provides theory data for ranges. + /// + public static IEnumerable RangeTheoryData => + new List + { + // (0,5) + new object[] { new NpgsqlRange(0, false, false, 5, false, false) }, + // [0,5] + new object[] { new NpgsqlRange(0, true, false, 5, true, false) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 0, false, true) }, + // (,) + new object[] { new NpgsqlRange(0, false, true, 5, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 0, false, true) }, + // (0,) + new object[] { new NpgsqlRange(0, false, false, 5, false, true) }, + // (,5) + new object[] { new NpgsqlRange(0, false, true, 5, false, false) } + }; + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing range operators. + /// + public class RangeQueryNpgsqlFixture : IDisposable + { + /// + /// The used for testing. + /// + private readonly NpgsqlTestStore _testStore; + + /// + /// The used for testing. + /// + private readonly DbContextOptions _options; + + /// + /// The logger factory used for testing. + /// + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } + + /// + /// Initializes a . + /// + // ReSharper disable once UnusedMember.Global + public RangeQueryNpgsqlFixture() + { + TestSqlLoggerFactory = new TestSqlLoggerFactory(); + + _testStore = NpgsqlTestStore.CreateScratch(); + + _options = + new DbContextOptionsBuilder() + .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkNpgsql() + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (RangeContext context = CreateContext()) + { + context.Database.EnsureCreated(); + + context.RangeTestEntities.AddRange( + new RangeTestEntity + { + Id = 1, + // (0, 10) + Range = new NpgsqlRange(0, false, false, 10, false, false), + }, + new RangeTestEntity + { + Id = 2, + // [0, 10) + Range = new NpgsqlRange(0, true, false, 10, false, false) + }, + new RangeTestEntity + { + Id = 3, + // [0, 10] + Range = new NpgsqlRange(0, true, false, 10, true, false) + }, + new RangeTestEntity + { + Id = 4, + // [0, ∞) + Range = new NpgsqlRange(0, true, false, 0, false, true) + }, + new RangeTestEntity + { + Id = 5, + // (-∞, 10] + Range = new NpgsqlRange(0, false, true, 10, true, false) + }, + new RangeTestEntity + { + Id = 6, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }, + new RangeTestEntity + { + Id = 7, + // (-∞, ∞) + Range = new NpgsqlRange(0, false, true, 0, false, true) + }); + + context.SaveChanges(); + } + } + + /// + /// Creates a new . + /// + /// + /// A for testing. + /// + public RangeContext CreateContext() + { + return new RangeContext(_options); + } + + /// + public void Dispose() + { + _testStore.Dispose(); + } + } + + /// + /// Represents an entity suitable for testing range operators. + /// + public class RangeTestEntity + { + /// + /// The primary key. + /// + public int Id { get; set; } + + /// + /// The range of integers. + /// + public NpgsqlRange Range { get; set; } + } + + /// + /// Represents a database suitable for testing range operators. + /// + public class RangeContext : DbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet RangeTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public RangeContext(DbContextOptions options) : base(options) { } + + /// + protected override void OnModelCreating(ModelBuilder builder) { } + } + + #endregion + + #region Helpers + + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + public void AssertContainsSql(string sql) + { + Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + } + + #endregion + } +}