diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs
index 1d2299ead..1184c23a0 100644
--- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs
@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal
@@ -6,7 +8,13 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal
public interface INpgsqlOptions : ISingletonOptions
{
///
- /// Reflects the option set by .
+ /// The backend version to target.
+ ///
+ [CanBeNull]
+ Version PostgresVersion { get; }
+
+ ///
+ /// True if reverse null ordering is enabled; otherwise, false.
///
bool ReverseNullOrderingEnabled { get; }
diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
index 52ce59361..0f7cc77b8 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
@@ -1,4 +1,5 @@
#region License
+
// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
@@ -19,8 +20,10 @@
// 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 System.Collections.Generic;
using System.Net.Security;
using JetBrains.Annotations;
@@ -33,16 +36,36 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal
{
public class NpgsqlOptionsExtension : RelationalOptionsExtension
{
- public string AdminDatabase { get; private set; }
- public bool? ReverseNullOrdering { get; private set; }
- public ProvideClientCertificatesCallback ProvideClientCertificatesCallback { get; private set; }
- public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; private set; }
+ readonly List _plugins;
+ public string AdminDatabase { get; private set; }
+ public bool ReverseNullOrdering { get; private set; }
+
+ ///
+ /// The backend version to target.
+ ///
+ [CanBeNull]
+ public Version PostgresVersion { get; private set; }
+
+ ///
+ /// The collection of database plugins.
+ ///
+ [NotNull]
+ [ItemNotNull]
public IReadOnlyList Plugins => _plugins;
- readonly List _plugins = new List();
+ ///
+ /// The specified .
+ ///
+ [CanBeNull]
+ public ProvideClientCertificatesCallback ProvideClientCertificatesCallback { get; private set; }
+
+ public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; private set; }
- public NpgsqlOptionsExtension() {}
+ ///
+ /// Initializes an instance of with the default settings.
+ ///
+ public NpgsqlOptionsExtension() => _plugins = new List();
// NB: When adding new options, make sure to update the copy ctor below.
@@ -51,9 +74,10 @@ public NpgsqlOptionsExtension([NotNull] NpgsqlOptionsExtension copyFrom)
{
AdminDatabase = copyFrom.AdminDatabase;
ReverseNullOrdering = copyFrom.ReverseNullOrdering;
+ PostgresVersion = copyFrom.PostgresVersion;
ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback;
RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback;
- _plugins.AddRange(copyFrom._plugins);
+ _plugins = new List(copyFrom._plugins);
}
protected override RelationalOptionsExtension Clone() => new NpgsqlOptionsExtension(this);
@@ -85,6 +109,28 @@ public virtual NpgsqlOptionsExtension WithAdminDatabase(string adminDatabase)
return clone;
}
+ ///
+ /// Returns a copy of the current instance with the specified PostgreSQL version.
+ ///
+ /// The backend version to target.
+ ///
+ /// A copy of the current instance with the specified PostgreSQL version.
+ ///
+ [NotNull]
+ public virtual NpgsqlOptionsExtension WithPostgresVersion([CanBeNull] Version postgresVersion)
+ {
+ var clone = (NpgsqlOptionsExtension)Clone();
+
+ clone.PostgresVersion = postgresVersion;
+
+ return clone;
+ }
+
+ ///
+ /// Returns a copy of the current instance configured with the specified value..
+ ///
+ /// True to enable reverse null ordering; otherwise, false.
+ [NotNull]
internal virtual NpgsqlOptionsExtension WithReverseNullOrdering(bool reverseNullOrdering)
{
var clone = (NpgsqlOptionsExtension)Clone();
diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
index 66e8497da..b66520a4f 100644
--- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
+++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
@@ -50,6 +50,13 @@ public virtual void UsePlugin(NpgsqlEntityFrameworkPlugin plugin)
///
public virtual void UseAdminDatabase(string dbName) => WithOption(e => e.WithAdminDatabase(dbName));
+ ///
+ /// Configures the backend version to target.
+ ///
+ /// The backend version to target.
+ public virtual void SetPostgresVersion([CanBeNull] Version postgresVersion)
+ => WithOption(e => e.WithPostgresVersion(postgresVersion));
+
///
/// Appends NULLS FIRST to all ORDER BY clauses. This is important for the tests which were written
/// for SQL Server. Note that to fully implement null-first ordering indexes also need to be generated
diff --git a/src/EFCore.PG/Internal/NpgsqlOptions.cs b/src/EFCore.PG/Internal/NpgsqlOptions.cs
index b036dc572..bd665c44c 100644
--- a/src/EFCore.PG/Internal/NpgsqlOptions.cs
+++ b/src/EFCore.PG/Internal/NpgsqlOptions.cs
@@ -10,11 +10,22 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal
{
public class NpgsqlOptions : INpgsqlOptions
{
+ ///
+ public virtual Version PostgresVersion { get; private set; }
+
+ ///
+ public virtual bool ReverseNullOrderingEnabled { get; private set; }
+
+ ///
+ public virtual IReadOnlyList Plugins { get; private set; }
+
+ ///
public void Initialize(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension();
- ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering ?? false;
+ PostgresVersion = npgsqlOptions.PostgresVersion;
+ ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering;
Plugins = npgsqlOptions.Plugins;
}
@@ -22,7 +33,7 @@ public void Validate(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension();
- if (ReverseNullOrderingEnabled != (npgsqlOptions.ReverseNullOrdering ?? false))
+ if (ReverseNullOrderingEnabled != npgsqlOptions.ReverseNullOrdering)
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
@@ -30,9 +41,5 @@ public void Validate(IDbContextOptions options)
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
}
-
- public virtual bool ReverseNullOrderingEnabled { get; private set; }
-
- public virtual IReadOnlyList Plugins { get; private set; }
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
index 5935a105d..993861271 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs
@@ -34,7 +34,6 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
{
new NpgsqlArraySequenceEqualTranslator(),
new NpgsqlConvertTranslator(),
- new NpgsqlDateAddTranslator(),
new NpgsqlStringSubstringTranslator(),
new NpgsqlLikeTranslator(),
new NpgsqlMathTranslator(),
@@ -60,14 +59,27 @@ public NpgsqlCompositeMethodCallTranslator(
[NotNull] INpgsqlOptions npgsqlOptions)
: base(dependencies)
{
+ var instanceTranslators =
+ new IMethodCallTranslator[]
+ {
+ new NpgsqlDateAddTranslator(npgsqlOptions.PostgresVersion)
+ };
+
// ReSharper disable once DoNotCallOverridableMethodsInConstructor
AddTranslators(_methodCallTranslators);
+ // ReSharper disable once DoNotCallOverridableMethodsInConstructor
+ AddTranslators(instanceTranslators);
+
foreach (var plugin in npgsqlOptions.Plugins)
plugin.AddMethodCallTranslators(this);
}
- public new virtual void AddTranslators([NotNull] IEnumerable translators)
+ ///
+ /// Adds additional dispatches to the translators list.
+ ///
+ /// The translators.
+ public new virtual void AddTranslators([NotNull] [ItemNotNull] IEnumerable translators)
=> base.AddTranslators(translators);
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateAddTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateAddTranslator.cs
index 4b0c9a43e..50321a4de 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateAddTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateAddTranslator.cs
@@ -1,16 +1,52 @@
-using System;
+#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 System.Collections.Generic;
-using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
{
+ ///
+ /// Provides expression translation for addition methods.
+ ///
public class NpgsqlDateAddTranslator : IMethodCallTranslator
{
- readonly Dictionary _methodInfoDatePartMapping = new Dictionary
+ ///
+ /// The minimum backend version supported by this translator.
+ ///
+ [NotNull] static readonly Version MinimumSupportedVersion = new Version(9, 4);
+
+ ///
+ /// The mapping of supported method translations.
+ ///
+ [NotNull] static readonly Dictionary MethodInfoDatePartMapping = new Dictionary
{
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) }), "years" },
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), new[] { typeof(int) }), "months" },
@@ -29,15 +65,25 @@ public class NpgsqlDateAddTranslator : IMethodCallTranslator
};
///
- /// Translates the given method call expression.
+ /// The backend version to target.
///
- /// The method call expression.
- ///
- /// A SQL expression representing the translated MethodCallExpression.
- ///
+ [NotNull] readonly Version _postgresVersion;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The backend version to target.
+ public NpgsqlDateAddTranslator([CanBeNull] Version postgresVersion)
+ => _postgresVersion = postgresVersion ?? MinimumSupportedVersion;
+
+ ///
public virtual Expression Translate(MethodCallExpression methodCallExpression)
{
- if (!_methodInfoDatePartMapping.TryGetValue(methodCallExpression.Method, out var datePart))
+ // This translation is only supported for PostgreSQL 9.4 or higher.
+ if (_postgresVersion < MinimumSupportedVersion)
+ return null;
+
+ if (!MethodInfoDatePartMapping.TryGetValue(methodCallExpression.Method, out var datePart))
return null;
// Ideally we would use Expression.Add() to have a simple plus-sign expression generated for us.
@@ -45,23 +91,27 @@ public virtual Expression Translate(MethodCallExpression methodCallExpression)
// We could use the provider-specific NpgsqlTimeSpan but it's not currently supported and is somewhat
// ugly, so we just generate the expression ourselves with CustomBinaryExpression
- var amountToAdd = methodCallExpression.Arguments.First();
+ Expression instance = methodCallExpression.Object;
+ Expression amountToAdd = methodCallExpression.Arguments[0];
+
+ if (instance is null || amountToAdd is null)
+ return null;
- if (amountToAdd is ConstantExpression constantExpression &&
- constantExpression.Type == typeof(double) &&
- ((double)constantExpression.Value >= int.MaxValue || (double)constantExpression.Value <= int.MinValue))
- {
+ if (amountToAdd is ConstantExpression amount &&
+ amount.Type == typeof(double) &&
+ ((double)amount.Value >= int.MaxValue ||
+ (double)amount.Value <= int.MinValue))
return null;
- }
- return new CustomBinaryExpression(
- methodCallExpression.Object,
- new PgFunctionExpression("MAKE_INTERVAL", typeof(TimeSpan), new Dictionary
- {
- { datePart, amountToAdd }
- }),
- "+",
- methodCallExpression.Type);
+ return
+ new CustomBinaryExpression(
+ instance,
+ new PgFunctionExpression(
+ "MAKE_INTERVAL",
+ typeof(TimeSpan),
+ new Dictionary { [datePart] = amountToAdd }),
+ "+",
+ methodCallExpression.Type);
}
}
}
diff --git a/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs
new file mode 100644
index 000000000..4f5af11a5
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Linq;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.TestUtilities;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+using Xunit;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query
+{
+ public class CompatibilityQueryNpgsqlTest : IClassFixture
+ {
+ ///
+ /// Provides resources for unit tests.
+ ///
+ CompatibilityQueryNpgsqlFixure Fixture { get; }
+
+ ///
+ /// Initializes resources for unit tests.
+ ///
+ /// The fixture of resources for testing.
+ public CompatibilityQueryNpgsqlTest(CompatibilityQueryNpgsqlFixure fixture)
+ {
+ Fixture = fixture;
+ Fixture.TestSqlLoggerFactory.Clear();
+ }
+
+ #region Tests
+
+ [Theory]
+ [InlineData("9.4")]
+ [InlineData("9.5")]
+ [InlineData("9.6")]
+ [InlineData("10.0")]
+ [InlineData("10.1")]
+ [InlineData("10.2")]
+ [InlineData("10.3")]
+ [InlineData("10.4")]
+ public void GivenDateTimeAdd_WhenVersionIsSupported_ThenTranslates(string version)
+ {
+ using (CompatibilityContext context = Fixture.CreateContext(postgresVersion: Version.Parse(version)))
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ int years = 2;
+
+ DateTime[] _ =
+ context.CompatibilityTestEntities
+ .Select(x => x.DateTime.AddYears(years))
+ .ToArray();
+
+ AssertContainsSql("SELECT (x.\"DateTime\" + MAKE_INTERVAL(years => @__years_0))");
+ }
+ }
+
+ [Theory]
+ [InlineData("9.0")]
+ [InlineData("9.1")]
+ [InlineData("9.2")]
+ [InlineData("9.3")]
+ public void GivenDateTimeAdd_WhenVersionIsNotSupported_ThenDoesNotTranslate(string version)
+ {
+ using (CompatibilityContext context = Fixture.CreateContext(postgresVersion: Version.Parse(version)))
+ {
+ // ReSharper disable once ConvertToConstant.Local
+ int years = 2;
+
+ DateTime[] _ =
+ context.CompatibilityTestEntities
+ .Select(x => x.DateTime.AddYears(years))
+ .ToArray();
+
+ AssertDoesNotContainsSql("SELECT (x.\"DateTime\" + MAKE_INTERVAL(years => @__years_0))");
+ }
+ }
+
+ #endregion
+
+ #region Fixtures
+
+ ///
+ /// Represents a fixture suitable for testing backendVersion.
+ ///
+ public class CompatibilityQueryNpgsqlFixure : IDisposable
+ {
+ ///
+ /// The used for testing.
+ ///
+ private readonly NpgsqlTestStore _testStore;
+
+ ///
+ /// The logger factory used for testing.
+ ///
+ public TestSqlLoggerFactory TestSqlLoggerFactory { get; }
+
+ ///
+ /// Initializes a .
+ ///
+ // ReSharper disable once UnusedMember.Global
+ public CompatibilityQueryNpgsqlFixure()
+ {
+ TestSqlLoggerFactory = new TestSqlLoggerFactory();
+
+ _testStore = NpgsqlTestStore.CreateScratch();
+
+ using (CompatibilityContext context = CreateContext())
+ {
+ context.Database.EnsureCreated();
+
+ context.CompatibilityTestEntities.AddRange(
+ new CompatibilityTestEntity
+ {
+ Id = 1,
+ DateTime = new DateTime(2018, 06, 23)
+ },
+ new CompatibilityTestEntity
+ {
+ Id = 2,
+ DateTime = new DateTime(2018, 06, 23)
+ },
+ new CompatibilityTestEntity
+ {
+ Id = 3,
+ DateTime = new DateTime(2018, 06, 23)
+ });
+
+ context.SaveChanges();
+ }
+ }
+
+ ///
+ public void Dispose() => _testStore.Dispose();
+
+ ///
+ /// Creates a new .
+ ///
+ /// The backend version to target.
+ ///
+ /// A for testing.
+ ///
+ public CompatibilityContext CreateContext(Version postgresVersion = null)
+ => new CompatibilityContext(
+ new DbContextOptionsBuilder()
+ .UseNpgsql(
+ _testStore.ConnectionString,
+ x =>
+ {
+ x.ApplyConfiguration();
+ if (postgresVersion != null)
+ x.SetPostgresVersion(postgresVersion);
+ })
+ .UseInternalServiceProvider(
+ new ServiceCollection()
+ .AddEntityFrameworkNpgsql()
+ .AddSingleton(TestSqlLoggerFactory)
+ .BuildServiceProvider())
+ .Options);
+ }
+
+ ///
+ /// Represents an entity suitable for testing backendVersion.
+ ///
+ public class CompatibilityTestEntity
+ {
+ ///
+ /// The primary key.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The date and time.
+ ///
+ public DateTime DateTime { get; set; }
+ }
+
+ ///
+ /// Represents a database suitable for testing range operators.
+ ///
+ public class CompatibilityContext : DbContext
+ {
+ ///
+ /// Represents a set of entities for backendVersion testing.
+ ///
+ public DbSet CompatibilityTestEntities { get; set; }
+
+ ///
+ public CompatibilityContext(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);
+ }
+
+ ///
+ /// Asserts that the SQL fragment does not appears in the logs.
+ ///
+ /// The SQL statement or fragment to search for in the logs.
+ public void AssertDoesNotContainsSql(string sql)
+ {
+ Assert.DoesNotContain(sql, Fixture.TestSqlLoggerFactory.Sql);
+ }
+
+ #endregion
+ }
+}