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
5 changes: 5 additions & 0 deletions src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public interface INpgsqlOptions : ISingletonOptions
/// </summary>
Version PostgresVersion { get; }

/// <summary>
/// Whether to target Redshift.
/// </summary>
bool UseRedshift { get; }

/// <summary>
/// True if reverse null ordering is enabled; otherwise, false.
/// </summary>
Expand Down
79 changes: 50 additions & 29 deletions src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal
/// </summary>
public class NpgsqlOptionsExtension : RelationalOptionsExtension
{
public static readonly Version DefaultPostgresVersion = new(12, 0);

private DbContextOptionsExtensionInfo? _info;
private readonly List<UserRangeDefinition> _userRangeDefinitions;

Expand All @@ -27,7 +29,12 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension
/// <summary>
/// The backend version to target.
/// </summary>
public virtual Version? PostgresVersion { get; private set; }
public virtual Version PostgresVersion { get; private set; } = DefaultPostgresVersion;

/// <summary>
/// Whether to target Redshift.
/// </summary>
public virtual bool UseRedshift { get; private set; }

/// <summary>
/// The list of range mappings specified by the user.
Expand Down Expand Up @@ -132,7 +139,23 @@ public virtual NpgsqlOptionsExtension WithPostgresVersion(Version? postgresVersi
{
var clone = (NpgsqlOptionsExtension)Clone();

clone.PostgresVersion = postgresVersion;
clone.PostgresVersion = postgresVersion ?? DefaultPostgresVersion;

return clone;
}

/// <summary>
/// Returns a copy of the current instance with the specified Redshift settings.
/// </summary>
/// <param name="useRedshift">Whether to target Redshift.</param>
/// <returns>
/// A copy of the current instance with the specified Redshift setting.
/// </returns>
public virtual NpgsqlOptionsExtension WithRedshift(bool useRedshift)
{
var clone = (NpgsqlOptionsExtension)Clone();

clone.UseRedshift = useRedshift;

return clone;
}
Expand All @@ -150,6 +173,17 @@ internal virtual NpgsqlOptionsExtension WithReverseNullOrdering(bool reverseNull
return clone;
}

/// <inheritdoc />
public override void Validate(IDbContextOptions options)
{
base.Validate(options);

if (UseRedshift && !PostgresVersion.Equals(DefaultPostgresVersion))
{
throw new InvalidOperationException($"{nameof(UseRedshift)} and {nameof(PostgresVersion)} cannot both be set");
}
}

#region Authentication

/// <summary>
Expand Down Expand Up @@ -234,11 +268,16 @@ public override string LogFragment
builder.Append(nameof(Extension.AdminDatabase)).Append("=").Append(Extension.AdminDatabase).Append(' ');
}

if (Extension.PostgresVersion != null)
if (!Extension.PostgresVersion.Equals(DefaultPostgresVersion))
{
builder.Append(nameof(Extension.PostgresVersion)).Append("=").Append(Extension.PostgresVersion).Append(' ');
}

if (Extension.UseRedshift)
{
builder.Append(nameof(Extension.UseRedshift)).Append(' ');
}

if (Extension.ProvideClientCertificatesCallback != null)
{
builder.Append(nameof(Extension.ProvideClientCertificatesCallback)).Append(" ");
Expand Down Expand Up @@ -296,6 +335,7 @@ public override long GetServiceProviderHashCode()
(h, ud) => (h * 397) ^ ud.GetHashCode());
_serviceProviderHash = (_serviceProviderHash * 397) ^ Extension.AdminDatabase?.GetHashCode() ?? 0L;
_serviceProviderHash = (_serviceProviderHash * 397) ^ (Extension.PostgresVersion?.GetHashCode() ?? 0L);
_serviceProviderHash = (_serviceProviderHash * 397) ^ Extension.UseRedshift.GetHashCode();
_serviceProviderHash = (_serviceProviderHash * 397) ^ (Extension.ProvideClientCertificatesCallback?.GetHashCode() ?? 0L);
_serviceProviderHash = (_serviceProviderHash * 397) ^ (Extension.RemoteCertificateValidationCallback?.GetHashCode() ?? 0L);
_serviceProviderHash = (_serviceProviderHash * 397) ^ (Extension.ProvidePasswordCallback?.GetHashCode() ?? 0L);
Expand All @@ -315,6 +355,9 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion)]
= (Extension.PostgresVersion?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture);

debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.UseRedshift)]
= Extension.UseRedshift.GetHashCode().ToString(CultureInfo.InvariantCulture);

debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.ReverseNullOrdering)]
= Extension.ReverseNullOrdering.GetHashCode().ToString(CultureInfo.InvariantCulture);

Expand All @@ -338,7 +381,10 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
#endregion Infrastructure
}

public class UserRangeDefinition : IEquatable<UserRangeDefinition>
/// <summary>
/// A definition for a user-defined PostgreSQL range to be mapped.
/// </summary>
public record UserRangeDefinition
{
/// <summary>
/// The name of the PostgreSQL range type to be mapped.
Expand Down Expand Up @@ -374,30 +420,5 @@ public UserRangeDefinition(
SubtypeClrType = Check.NotNull(subtypeClrType, nameof(subtypeClrType));
SubtypeName = subtypeName;
}

public override int GetHashCode()
=> HashCode.Combine(RangeName, SchemaName, SubtypeClrType, SubtypeName);

public override bool Equals(object? obj) => obj is UserRangeDefinition urd && Equals(urd);

public virtual bool Equals(UserRangeDefinition? other)
=> ReferenceEquals(this, other) ||
!(other is null) &&
RangeName == other.RangeName &&
SchemaName == other.SchemaName &&
SubtypeClrType == other.SubtypeClrType &&
SubtypeName == other.SubtypeName;

public virtual void Deconstruct(
out string rangeName,
out string? schemaName,
out Type subtypeClrType,
out string? subtypeName)
{
rangeName = RangeName;
schemaName = SchemaName;
subtypeClrType = SubtypeClrType;
subtypeName = SubtypeName;
}
}
}
8 changes: 8 additions & 0 deletions src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(Version? postgre
public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(int major, int minor)
=> SetPostgresVersion(new Version(major, minor));

/// <summary>
/// Configures the provider to work in Redshift compatibility mode, which avoids certain unsupported features from modern
/// PostgreSQL versions.
/// </summary>
/// <param name="useRedshift">Whether to target Redshift.</param>
public virtual NpgsqlDbContextOptionsBuilder UseRedshift(bool useRedshift = true)
=> WithOption(e => e.WithRedshift(useRedshift));

/// <summary>
/// Maps a user-defined PostgreSQL range type for use.
/// </summary>
Expand Down
34 changes: 31 additions & 3 deletions src/EFCore.PG/Internal/NpgsqlOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -11,11 +12,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal
/// <inheritdoc />
public class NpgsqlOptions : INpgsqlOptions
{
public static readonly Version DefaultPostgresVersion = new(12, 0);

/// <inheritdoc />
public virtual Version PostgresVersion { get; private set; } = null!;

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

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

Expand All @@ -30,7 +32,8 @@ public virtual void Initialize(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension<NpgsqlOptionsExtension>() ?? new NpgsqlOptionsExtension();

PostgresVersion = npgsqlOptions.PostgresVersion ?? DefaultPostgresVersion;
PostgresVersion = npgsqlOptions.PostgresVersion;
UseRedshift = npgsqlOptions.UseRedshift;
ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering;
UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions;
}
Expand All @@ -40,13 +43,38 @@ public virtual void Validate(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension<NpgsqlOptionsExtension>() ?? new NpgsqlOptionsExtension();

if (!PostgresVersion.Equals(npgsqlOptions.PostgresVersion))
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}

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

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

if (UserRangeDefinitions.Count != npgsqlOptions.UserRangeDefinitions.Count
|| UserRangeDefinitions.Zip(npgsqlOptions.UserRangeDefinitions).Any(t => t.First != t.Second))
{
throw new InvalidOperationException(
CoreStrings.SingletonOptionChanged(
nameof(NpgsqlDbContextOptionsBuilder.MapRange),
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -35,18 +34,18 @@ public class NpgsqlArrayTranslator : IMethodCallTranslator, IMemberTranslator
typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1);

private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly NpgsqlJsonPocoTranslator _jsonPocoTranslator;
private readonly bool _useRedshift;

public NpgsqlArrayTranslator(
IRelationalTypeMappingSource typeMappingSource,
NpgsqlSqlExpressionFactory sqlExpressionFactory,
NpgsqlJsonPocoTranslator jsonPocoTranslator)
NpgsqlJsonPocoTranslator jsonPocoTranslator,
bool useRedshift)
{
_typeMappingSource = typeMappingSource;
_sqlExpressionFactory = sqlExpressionFactory;
_jsonPocoTranslator = jsonPocoTranslator;
_useRedshift = useRedshift;
}

public virtual SqlExpression? Translate(
Expand Down Expand Up @@ -94,8 +93,8 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList)
if (method.IsClosedFormOf(EnumerableAnyWithoutPredicate))
{
return _sqlExpressionFactory.GreaterThan(
_jsonPocoTranslator.TranslateArrayLength(arrayOrList) ??
_sqlExpressionFactory.Function(
_jsonPocoTranslator.TranslateArrayLength(arrayOrList)
?? _sqlExpressionFactory.Function(
"cardinality",
new[] { arrayOrList },
nullable: true,
Expand All @@ -108,17 +107,18 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList)
// is pattern-matched in AllAnyToContainsRewritingExpressionVisitor, which transforms it to
// new[] { "a", "b", "c" }.Contains(e.Some Text).

if ((method.IsClosedFormOf(EnumerableContains) || // Enumerable.Contains extension method
method.Name == nameof(List<int>.Contains) && method.DeclaringType.IsGenericList() &&
method.GetParameters().Length == 1)
if ((method.IsClosedFormOf(EnumerableContains)
||
method.Name == nameof(List<int>.Contains)
&& method.DeclaringType.IsGenericList()
&& method.GetParameters().Length == 1)
&&
(
// Handle either array columns (with an array mapping) or parameters/constants (no mapping). We specifically
// don't want to translate if the type mapping is bytea (CLR type is array, but not an array in
// the database).
// arrayOrList.TypeMapping == null && _typeMappingSource.FindMapping(arrayOrList.Type) != null ||
arrayOrList.TypeMapping is NpgsqlArrayTypeMapping or null
))
// Handle either array columns (with an array mapping) or parameters/constants (no mapping). We specifically
// don't want to translate if the type mapping is bytea (CLR type is array, but not an array in
// the database).
// arrayOrList.TypeMapping == null && _typeMappingSource.FindMapping(arrayOrList.Type) != null ||
arrayOrList.TypeMapping is NpgsqlArrayTypeMapping or null
&& !_useRedshift)
{
var item = arguments[0];

Expand All @@ -142,7 +142,8 @@ arrayOrList.TypeMapping is NpgsqlArrayTypeMapping or null
typeof(int)));
}

return _sqlExpressionFactory.Contains(arrayOrList,
return _sqlExpressionFactory.Contains(
arrayOrList,
_sqlExpressionFactory.NewArrayOrConstant(new[] { item }, arrayOrList.Type));

// Don't do anything PG-specific for constant arrays since the general EF Core mechanism is fine
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
{
Expand All @@ -12,15 +13,16 @@ public class NpgsqlMemberTranslatorProvider : RelationalMemberTranslatorProvider

public NpgsqlMemberTranslatorProvider(
RelationalMemberTranslatorProviderDependencies dependencies,
IRelationalTypeMappingSource typeMappingSource)
IRelationalTypeMappingSource typeMappingSource,
INpgsqlOptions npgsqlOptions)
: base(dependencies)
{
var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory;
JsonPocoTranslator = new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory);

AddTranslators(
new IMemberTranslator[] {
new NpgsqlArrayTranslator(typeMappingSource, sqlExpressionFactory, JsonPocoTranslator),
new NpgsqlArrayTranslator(sqlExpressionFactory, JsonPocoTranslator, npgsqlOptions.UseRedshift),
new NpgsqlDateTimeMemberTranslator(sqlExpressionFactory),
new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public NpgsqlMethodCallTranslatorProvider(

AddTranslators(new IMethodCallTranslator[]
{
new NpgsqlArrayTranslator(typeMappingSource, npgsqlSqlExpressionFactory, jsonTranslator),
new NpgsqlArrayTranslator(npgsqlSqlExpressionFactory, jsonTranslator, npgsqlOptions.UseRedshift),
new NpgsqlByteArrayMethodTranslator(npgsqlSqlExpressionFactory),
new NpgsqlConvertTranslator(npgsqlSqlExpressionFactory),
new NpgsqlDateTimeMethodTranslator(typeMappingSource, npgsqlSqlExpressionFactory),
Expand Down
Loading