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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<EFCoreVersion>10.0.0-preview.6.25321.102</EFCoreVersion>
<MicrosoftExtensionsVersion>10.0.0-preview.6.25321.102</MicrosoftExtensionsVersion>
<EFCoreVersion>10.0.0-preview.7.25352.2</EFCoreVersion>
<MicrosoftExtensionsVersion>10.0.0-preview.7.25351.105</MicrosoftExtensionsVersion>
<NpgsqlVersion>9.0.3</NpgsqlVersion>
</PropertyGroup>

Expand Down
27 changes: 26 additions & 1 deletion src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
public class NpgsqlOptionsExtension : RelationalOptionsExtension
{
private DbContextOptionsExtensionInfo? _info;
private ParameterizedCollectionMode? _parameterizedCollectionMode;

private readonly List<UserRangeDefinition> _userRangeDefinitions;
private readonly List<EnumDefinition> _enumDefinitions;

private Version? _postgresVersion;

// We override ParameterizedCollectionMode to set Parameter as the default instead of MultipleParameters,
// which is the EF relational default. In PostgreSQL using native array parameters is better, and the
// query plan problem can be mitigated by telling PostgreSQL to use a custom plan (see #3269).

/// <summary>
/// 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.
/// </summary>
public override ParameterizedCollectionMode ParameterizedCollectionMode
=> _parameterizedCollectionMode ?? ParameterizedCollectionMode.Parameter;

/// <summary>
/// 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
Expand Down Expand Up @@ -132,6 +146,17 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom)
public override int? MinBatchSize
=> base.MinBatchSize ?? 2;

// We need to override WithUseParameterizedCollectionMode since we override ParameterizedCollectionMode above
/// <inheritdoc />
public override RelationalOptionsExtension WithUseParameterizedCollectionMode(ParameterizedCollectionMode parameterizedCollectionMode)
{
var clone = (NpgsqlOptionsExtension)Clone();

clone._parameterizedCollectionMode = parameterizedCollectionMode;

return clone;
}

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
Expand Down
20 changes: 4 additions & 16 deletions src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,16 @@ public NpgsqlParameterBasedSqlProcessor(
/// 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.
/// </summary>
public override Expression Optimize(
Expression queryExpression,
IReadOnlyDictionary<string, object?> parametersValues,
out bool canCache)
public override Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade)
{
queryExpression = base.Optimize(queryExpression, parametersValues, out canCache);
queryExpression = base.Process(queryExpression, parametersFacade);

queryExpression = new NpgsqlDeleteConvertingExpressionVisitor().Process(queryExpression);

return queryExpression;
}

/// <inheritdoc />
protected override Expression ProcessSqlNullability(
Expression selectExpression,
IReadOnlyDictionary<string, object?> parametersValues,
out bool canCache)
{
Check.NotNull(selectExpression, nameof(selectExpression));
Check.NotNull(parametersValues, nameof(parametersValues));

return new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process(
selectExpression, parametersValues, out canCache);
}
protected override Expression ProcessSqlNullability(Expression selectExpression, CacheSafeParameterFacade parametersFacade)
=> new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersFacade);
}
Original file line number Diff line number Diff line change
Expand Up @@ -737,5 +737,5 @@ private static bool MayContainNulls(SqlExpression arrayExpression)
// Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
private bool IsNull(SqlExpression? expression)
=> expression is SqlConstantExpression { Value: null }
|| expression is SqlParameterExpression { Name: string parameterName } && ParameterValues[parameterName] is null;
|| expression is SqlParameterExpression { Name: string parameterName } && ParametersFacade.IsParameterNull(parameterName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ private bool TryTranslateStartsEndsWithContains(
QueryContext queryContext,
string baseParameterName,
StartsEndsWithContains methodType)
=> queryContext.ParameterValues[baseParameterName] switch
=> queryContext.Parameters[baseParameterName] switch
{
null => null,

Expand Down
105 changes: 63 additions & 42 deletions src/Shared/Check.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Utilities;
Expand All @@ -8,13 +14,13 @@ internal static class Check
{
[ContractAnnotation("value:null => halt")]
[return: NotNull]
public static T NotNull<T>([NoEnumeration] [AllowNull] [NotNull] T value, [InvokerParameterName] string parameterName)
public static T NotNull<T>(
[NoEnumeration, AllowNull, NotNull] T value,
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
if (value is null)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentNullException(parameterName);
ThrowArgumentNull(parameterName);
}

return value;
Expand All @@ -23,108 +29,123 @@ public static T NotNull<T>([NoEnumeration] [AllowNull] [NotNull] T value, [Invok
[ContractAnnotation("value:null => halt")]
public static IReadOnlyList<T> NotEmpty<T>(
[NotNull] IReadOnlyList<T>? value,
[InvokerParameterName] string parameterName)
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
NotNull(value, parameterName);

if (value.Count == 0)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentException(AbstractionsStrings.CollectionArgumentIsEmpty(parameterName));
ThrowNotEmpty(parameterName);
}

return value;
}

[ContractAnnotation("value:null => halt")]
public static string NotEmpty([NotNull] string? value, [InvokerParameterName] string parameterName)
public static string NotEmpty(
[NotNull] string? value,
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
if (value is null)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentNullException(parameterName);
}
NotNull(value, parameterName);

if (value.Trim().Length == 0)
if (value.AsSpan().Trim().Length == 0)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName));
ThrowStringArgumentEmpty(parameterName);
}

return value;
}

public static string? NullButNotEmpty(string? value, [InvokerParameterName] string parameterName)
public static IReadOnlyCollection<T>? NullButNotEmpty<T>(
IReadOnlyCollection<T>? value,
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
if (value is not null && value.Length == 0)
if (value is not null && value.Count == 0)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName));
ThrowStringArgumentEmpty(parameterName);
}

return value;
}

public static IReadOnlyCollection<T>? NullButNotEmpty<T>(
IReadOnlyCollection<T>? value,
[InvokerParameterName] string parameterName)
public static string? NullButNotEmpty(
string? value,
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
if (value is { Count: 0 })
if (value is not null && value.Length == 0)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName));
ThrowStringArgumentEmpty(parameterName);
}

return value;
}

public static IReadOnlyList<T> HasNoNulls<T>(
[NotNull] IReadOnlyList<T>? value,
[InvokerParameterName] string parameterName)
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
where T : class
{
NotNull(value, parameterName);

if (value.Any(e => e is null))
for (var i = 0; i < value.Count; i++)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentException(parameterName);
if (value[i] is null)
{
ThrowArgumentException(parameterName, parameterName);
}
}

return value;
}

public static IReadOnlyList<string> HasNoEmptyElements(
[NotNull] IReadOnlyList<string>? value,
[InvokerParameterName] string parameterName)
[InvokerParameterName, CallerArgumentExpression(nameof(value))] string parameterName = "")
{
NotNull(value, parameterName);

if (value.Any(s => string.IsNullOrWhiteSpace(s)))
for (var i = 0; i < value.Count; i++)
{
NotEmpty(parameterName, nameof(parameterName));

throw new ArgumentException(AbstractionsStrings.CollectionArgumentHasEmptyElements(parameterName));
if (string.IsNullOrWhiteSpace(value[i]))
{
ThrowCollectionHasEmptyElements(parameterName);
}
}

return value;
}

[Conditional("DEBUG")]
public static void DebugAssert([DoesNotReturnIf(false)] bool condition, string message)
public static void DebugAssert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string message = "")
{
if (!condition)
{
throw new Exception($"Check.DebugAssert failed: {message}");
throw new UnreachableException($"Check.DebugAssert failed: {message}");
}
}

[Conditional("DEBUG")]
[DoesNotReturn]
public static void DebugFail(string message)
=> throw new Exception($"Check.DebugFail failed: {message}");
=> throw new UnreachableException($"Check.DebugFail failed: {message}");

[DoesNotReturn]
private static void ThrowArgumentNull(string parameterName)
=> throw new ArgumentNullException(parameterName);

[DoesNotReturn]
private static void ThrowNotEmpty(string parameterName)
=> throw new ArgumentException(AbstractionsStrings.CollectionArgumentIsEmpty, parameterName);

[DoesNotReturn]
private static void ThrowStringArgumentEmpty(string parameterName)
=> throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty, parameterName);

[DoesNotReturn]
private static void ThrowCollectionHasEmptyElements(string parameterName)
=> throw new ArgumentException(AbstractionsStrings.CollectionArgumentHasEmptyElements, parameterName);

[DoesNotReturn]
private static void ThrowArgumentException(string message, string parameterName)
=> throw new ArgumentException(message, parameterName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ public class AdHocMiscellaneousQueryNpgsqlTest(NonSharedFixture fixture) : AdHoc
protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.Instance;

protected override DbContextOptionsBuilder SetTranslateParameterizedCollectionsToConstants(DbContextOptionsBuilder optionsBuilder)
protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode)
{
new NpgsqlDbContextOptionsBuilder(optionsBuilder).TranslateParameterizedCollectionsToConstants();
new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode);

return optionsBuilder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ public async Task Array_contains_is_not_parameterized_with_array_on_redshift()
{
var ctx = CreateRedshiftContext();

var numbers = new[] { 8, 9 };
var result = await ctx.TestEntities.Where(e => numbers.Contains(e.SomeInt)).SingleAsync();
Assert.Equal(1, result.Id);

AssertSql(
"""
SELECT t."Id", t."SomeInt"
FROM "TestEntities" AS t
WHERE t."SomeInt" IN (?, ?)
LIMIT 2
""");
// https://github.com/dotnet/efcore/issues/36311
await Assert.ThrowsAsync<UnreachableException>(async () =>
{
var numbers = new[] { 8, 9 };
var result = await ctx.TestEntities.Where(e => numbers.Contains(e.SomeInt)).SingleAsync();
Assert.Equal(1, result.Id);

AssertSql(
"""
SELECT t."Id", t."SomeInt"
FROM "TestEntities" AS t
WHERE t."SomeInt" IN (?, ?)
LIMIT 2
""");
});
}

#region Support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,4 @@ public override Task GroupJoin_client_method_in_OrderBy(bool async)
CoreStrings.QueryUnableToTranslateMethod(
"Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase<Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryNpgsqlFixture>",
"ClientMethodNullableInt"));

public override Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async)
=> Assert.ThrowsAsync<EqualException>(
async () => await base.Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(async));
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ public override Task GroupJoin_client_method_in_OrderBy(bool async)
"Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase<Microsoft.EntityFrameworkCore.Query.ComplexNavigationsSharedTypeQueryNpgsqlFixture>",
"ClientMethodNullableInt"));

public override Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async)
=> Assert.ThrowsAsync<EqualException>(
async () => await base.Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(async));

[ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26104")]
public override Task GroupBy_aggregate_where_required_relationship(bool async)
=> base.GroupBy_aggregate_where_required_relationship(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ namespace Microsoft.EntityFrameworkCore.Query;
public class NonSharedPrimitiveCollectionsQueryNpgsqlTest(NonSharedFixture fixture)
: NonSharedPrimitiveCollectionsQueryRelationalTestBase(fixture)
{
protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode)
{
new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode);

return optionsBuilder;
}

#region Support for specific element types

// Since we just use arrays for primitive collections, there's no need to test each and every element type; arrays are fully typed
Expand Down Expand Up @@ -95,20 +102,6 @@ LIMIT 2
""");
}

protected override DbContextOptionsBuilder SetTranslateParameterizedCollectionsToConstants(DbContextOptionsBuilder optionsBuilder)
{
new NpgsqlDbContextOptionsBuilder(optionsBuilder).TranslateParameterizedCollectionsToConstants();

return optionsBuilder;
}

protected override DbContextOptionsBuilder SetTranslateParameterizedCollectionsToParameters(DbContextOptionsBuilder optionsBuilder)
{
new NpgsqlDbContextOptionsBuilder(optionsBuilder).TranslateParameterizedCollectionsToParameters();

return optionsBuilder;
}

protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.Instance;
}
Loading