Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal
{
public interface INpgsqlOptions : ISingletonOptions
{
/// <summary>
/// Reflects the option set by <see cref="NpgsqlDbContextOptionsBuilder.ReverseNullOrdering" />.
/// The backend version to target.
/// </summary>
[CanBeNull]
Version PostgresVersion { get; }

/// <summary>
/// True if reverse null ordering is enabled; otherwise, false.
/// </summary>
bool ReverseNullOrderingEnabled { get; }

Expand Down
60 changes: 53 additions & 7 deletions src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#region License

// The PostgreSQL License
//
// Copyright (C) 2016 The Npgsql Development Team
Expand All @@ -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;
Expand All @@ -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<NpgsqlEntityFrameworkPlugin> _plugins;

public string AdminDatabase { get; private set; }
public bool ReverseNullOrdering { get; private set; }

/// <summary>
/// The backend version to target.
/// </summary>
[CanBeNull]
public Version PostgresVersion { get; private set; }

/// <summary>
/// The collection of database plugins.
/// </summary>
[NotNull]
[ItemNotNull]
public IReadOnlyList<NpgsqlEntityFrameworkPlugin> Plugins => _plugins;

readonly List<NpgsqlEntityFrameworkPlugin> _plugins = new List<NpgsqlEntityFrameworkPlugin>();
/// <summary>
/// The specified <see cref="ProvideClientCertificatesCallback"/>.
/// </summary>
[CanBeNull]
public ProvideClientCertificatesCallback ProvideClientCertificatesCallback { get; private set; }

public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; private set; }

public NpgsqlOptionsExtension() {}
/// <summary>
/// Initializes an instance of <see cref="NpgsqlOptionsExtension"/> with the default settings.
/// </summary>
public NpgsqlOptionsExtension() => _plugins = new List<NpgsqlEntityFrameworkPlugin>();

// NB: When adding new options, make sure to update the copy ctor below.

Expand All @@ -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<NpgsqlEntityFrameworkPlugin>(copyFrom._plugins);
}

protected override RelationalOptionsExtension Clone() => new NpgsqlOptionsExtension(this);
Expand Down Expand Up @@ -85,6 +109,28 @@ public virtual NpgsqlOptionsExtension WithAdminDatabase(string adminDatabase)
return clone;
}

/// <summary>
/// Returns a copy of the current instance with the specified PostgreSQL version.
/// </summary>
/// <param name="postgresVersion">The backend version to target.</param>
/// <returns>
/// A copy of the current instance with the specified PostgreSQL version.
/// </returns>
[NotNull]
public virtual NpgsqlOptionsExtension WithPostgresVersion([CanBeNull] Version postgresVersion)
{
var clone = (NpgsqlOptionsExtension)Clone();

clone.PostgresVersion = postgresVersion;

return clone;
}

/// <summary>
/// Returns a copy of the current instance configured with the specified value..
/// </summary>
/// <param name="reverseNullOrdering">True to enable reverse null ordering; otherwise, false.</param>
[NotNull]
internal virtual NpgsqlOptionsExtension WithReverseNullOrdering(bool reverseNullOrdering)
{
var clone = (NpgsqlOptionsExtension)Clone();
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ public virtual void UsePlugin(NpgsqlEntityFrameworkPlugin plugin)
/// </summary>
public virtual void UseAdminDatabase(string dbName) => WithOption(e => e.WithAdminDatabase(dbName));

/// <summary>
/// Configures the backend version to target.
/// </summary>
/// <param name="postgresVersion">The backend version to target.</param>
public virtual void SetPostgresVersion([CanBeNull] Version postgresVersion)
=> WithOption(e => e.WithPostgresVersion(postgresVersion));

/// <summary>
/// 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
Expand Down
19 changes: 13 additions & 6 deletions src/EFCore.PG/Internal/NpgsqlOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,36 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal
{
public class NpgsqlOptions : INpgsqlOptions
{
/// <inheritdoc />
public virtual Version PostgresVersion { get; private set; }

/// <inheritdoc />
public virtual bool ReverseNullOrderingEnabled { get; private set; }

/// <inheritdoc />
public virtual IReadOnlyList<NpgsqlEntityFrameworkPlugin> Plugins { get; private set; }

/// <inheritdoc />
public void Initialize(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension<NpgsqlOptionsExtension>() ?? new NpgsqlOptionsExtension();

ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering ?? false;
PostgresVersion = npgsqlOptions.PostgresVersion;
ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering;
Plugins = npgsqlOptions.Plugins;
}

public void Validate(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension<NpgsqlOptionsExtension>() ?? new NpgsqlOptionsExtension();

if (ReverseNullOrderingEnabled != (npgsqlOptions.ReverseNullOrdering ?? false))
if (ReverseNullOrderingEnabled != npgsqlOptions.ReverseNullOrdering)
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
nameof(NpgsqlDbContextOptionsBuilder.ReverseNullOrdering),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
}

public virtual bool ReverseNullOrderingEnabled { get; private set; }

public virtual IReadOnlyList<NpgsqlEntityFrameworkPlugin> Plugins { get; private set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
{
new NpgsqlArraySequenceEqualTranslator(),
new NpgsqlConvertTranslator(),
new NpgsqlDateAddTranslator(),
new NpgsqlStringSubstringTranslator(),
new NpgsqlLikeTranslator(),
new NpgsqlMathTranslator(),
Expand All @@ -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<IMethodCallTranslator> translators)
/// <summary>
/// Adds additional dispatches to the translators list.
/// </summary>
/// <param name="translators">The translators.</param>
public new virtual void AddTranslators([NotNull] [ItemNotNull] IEnumerable<IMethodCallTranslator> translators)
=> base.AddTranslators(translators);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides expression translation for <see cref="DateTime"/> addition methods.
/// </summary>
public class NpgsqlDateAddTranslator : IMethodCallTranslator
{
readonly Dictionary<MethodInfo, string> _methodInfoDatePartMapping = new Dictionary<MethodInfo, string>
/// <summary>
/// The minimum backend version supported by this translator.
/// </summary>
[NotNull] static readonly Version MinimumSupportedVersion = new Version(9, 4);

/// <summary>
/// The mapping of supported method translations.
/// </summary>
[NotNull] static readonly Dictionary<MethodInfo, string> MethodInfoDatePartMapping = new Dictionary<MethodInfo, string>
{
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) }), "years" },
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMonths), new[] { typeof(int) }), "months" },
Expand All @@ -29,39 +65,53 @@ public class NpgsqlDateAddTranslator : IMethodCallTranslator
};

/// <summary>
/// Translates the given method call expression.
/// The backend version to target.
/// </summary>
/// <param name="methodCallExpression">The method call expression.</param>
/// <returns>
/// A SQL expression representing the translated MethodCallExpression.
/// </returns>
[NotNull] readonly Version _postgresVersion;

/// <summary>
/// Initializes a new instance of the <see cref="NpgsqlDateAddTranslator"/> class.
/// </summary>
/// <param name="postgresVersion">The backend version to target.</param>
public NpgsqlDateAddTranslator([CanBeNull] Version postgresVersion)
=> _postgresVersion = postgresVersion ?? MinimumSupportedVersion;

/// <inheritdoc />
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.
// But to represent an interval we'd need to use a TimeSpan, which does not represent months/years.
// 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<string, Expression>
{
{ datePart, amountToAdd }
}),
"+",
methodCallExpression.Type);
return
new CustomBinaryExpression(
instance,
new PgFunctionExpression(
"MAKE_INTERVAL",
typeof(TimeSpan),
new Dictionary<string, Expression> { [datePart] = amountToAdd }),
"+",
methodCallExpression.Type);
}
}
}
Loading