From ed8d03250c3ff53f6fec1de4acfc5c26166e4698 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 25 Mar 2024 10:26:30 +0100 Subject: [PATCH] Sync to 9.0.0-preview.3.24172.4 * Implement quoting for custom SqlExpressions * Type mapping visitor changes * React to SqlConstantExpression changes --- .github/workflows/build.yml | 2 +- Directory.Build.props | 2 +- Directory.Packages.props | 10 +- EFCore.PG.sln.DotSettings | 207 +----------------- global.json | 4 +- .../PendingDateTimeZoneProviderExpression.cs | 3 + .../PendingZonedDateTimeExpression.cs | 3 + src/EFCore.PG/EFCore.PG.csproj | 1 + .../NpgsqlObjectToStringTranslator.cs | 2 +- .../Expressions/Internal/PgAllExpression.cs | 12 + .../Expressions/Internal/PgAnyExpression.cs | 12 + .../Internal/PgArrayIndexExpression.cs | 13 ++ .../Internal/PgArraySliceExpression.cs | 14 ++ .../Internal/PgBinaryExpression.cs | 13 ++ .../Expressions/Internal/PgILikeExpression.cs | 12 + .../Internal/PgJsonTraversalExpression.cs | 13 ++ .../Internal/PgNewArrayExpression.cs | 11 + .../Internal/PgRegexMatchExpression.cs | 12 + .../Internal/PgRowValueExpression.cs | 16 +- .../Internal/PgUnknownBinaryExpression.cs | 13 ++ .../Query/Internal/NpgsqlQuerySqlGenerator.cs | 3 +- .../NpgsqlQueryTranslationPostprocessor.cs | 9 + ...yableMethodTranslatingExpressionVisitor.cs | 70 ------ .../Internal/NpgsqlSqlNullabilityProcessor.cs | 2 +- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 4 +- .../NpgsqlTypeMappingPostprocessor.cs | 60 +++++ .../Query/NpgsqlSqlExpressionFactory.cs | 4 +- test/Directory.Build.props | 2 +- .../Migrations/MigrationsNpgsqlTest.cs | 163 ++++++++++++++ .../TestUtilities/NpgsqlTestStore.cs | 74 +++---- 30 files changed, 436 insertions(+), 330 deletions(-) create mode 100644 src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dcb5e9e6..cabcf202d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: pull_request: env: - dotnet_sdk_version: '8.0.100' + dotnet_sdk_version: '9.0.100-preview.3.24204.13' postgis_version: 3 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true diff --git a/Directory.Build.props b/Directory.Build.props index b913e4b87..5fb8849de 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ true true - Copyright 2023 © The Npgsql Development Team + Copyright 2024 © The Npgsql Development Team Npgsql true PostgreSQL diff --git a/Directory.Packages.props b/Directory.Packages.props index 93aecba8c..175761c59 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - 9.0.0-preview.2.24128.4 - 9.0.0-preview.2.24128.5 + 9.0.0-preview.3.24172.4 + 9.0.0-preview.3.24172.9 8.0.2 @@ -23,8 +23,10 @@ - - + + + + diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 0ae73aa7c..9055198e9 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -182,216 +182,11 @@ True True - - True - True - True - Side by side - Side by side - False - False - False - True - False - False - True - False - False - False - True - <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> - True - FK - MARS - $object$_On$event$ - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Methods"><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="Aa_bb" /></Policy> - EF - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> - $object$_On$event$ - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - - - True - C:\repos\EntityFrameworkCore\All.sln.DotSettings - - - - True - 2 - DO_NOTHING - True - True True - True True - True True - True True True - True - True - True - True True -<<<<<<< HEAD - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - + \ No newline at end of file diff --git a/global.json b/global.json index 817f0c380..50d6cea52 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "8.0.100-rc.2.23502.2", + "version": "9.0.100-preview.3.24204.13", "rollForward": "latestMajor", - "allowPrerelease": "true" + "allowPrerelease": true } } diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs b/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs index 11879f214..7c2ee5ef9 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs @@ -9,6 +9,9 @@ private PendingDateTimeZoneProviderExpression() { } + public override Expression Quote() + => throw new UnreachableException("PendingDateTimeZoneProviderExpression is a temporary tree representation and should never be quoted"); + protected override void Print(ExpressionPrinter expressionPrinter) => expressionPrinter.Append("TZDB"); } diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs b/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs index 35c8affee..41739d713 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs +++ b/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs @@ -12,6 +12,9 @@ internal PendingZonedDateTimeExpression(SqlExpression operand, SqlExpression tim internal SqlExpression TimeZoneId { get; } + public override Expression Quote() + => throw new UnreachableException("PendingDateTimeZoneProviderExpression is a temporary tree representation and should never be quoted"); + protected override void Print(ExpressionPrinter expressionPrinter) { expressionPrinter.Visit(Operand); diff --git a/src/EFCore.PG/EFCore.PG.csproj b/src/EFCore.PG/EFCore.PG.csproj index 0ab1ace12..1fb3139f4 100644 --- a/src/EFCore.PG/EFCore.PG.csproj +++ b/src/EFCore.PG/EFCore.PG.csproj @@ -9,6 +9,7 @@ PostgreSQL/Npgsql provider for Entity Framework Core. npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql README.md + EF1003 diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs index 8478cae09..377939774 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs @@ -78,7 +78,7 @@ public NpgsqlObjectToStringTranslator(IRelationalTypeMappingSource typeMappingSo _sqlExpressionFactory.Equal(instance, _sqlExpressionFactory.Constant(true)), _sqlExpressionFactory.Constant(true.ToString())) }, - _sqlExpressionFactory.Constant(null)) + _sqlExpressionFactory.Constant(null, typeof(string))) : _sqlExpressionFactory.Case( new[] { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs index 714b895ac..0ee27abd3 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs @@ -8,6 +8,8 @@ /// public class PgAllExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -63,6 +65,16 @@ public virtual PgAllExpression Update(SqlExpression item, SqlExpression array) ? new PgAllExpression(item, array, OperatorType, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgAllExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAllOperatorType), typeof(RelationalTypeMapping)])!, + Item.Quote(), + Array.Quote(), + Constant(OperatorType), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Item), (SqlExpression)visitor.Visit(Array)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs index 94c2330dd..3d57bc228 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs @@ -11,6 +11,8 @@ /// public class PgAnyExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -74,6 +76,16 @@ public virtual PgAnyExpression Update(SqlExpression item, SqlExpression array) ? new PgAnyExpression(item, array, OperatorType, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgAnyExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(PgAllOperatorType), typeof(RelationalTypeMapping)])!, + Item.Quote(), + Array.Quote(), + Constant(OperatorType), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Item), (SqlExpression)visitor.Visit(Array)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs index 4a4f3d51b..d3e676fb7 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs @@ -9,6 +9,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgArrayIndexExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The array being indexed. /// @@ -75,6 +77,17 @@ public virtual PgArrayIndexExpression Update(SqlExpression array, SqlExpression ? this : new PgArrayIndexExpression(array, index, IsNullable, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgArrayIndexExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Array.Quote(), + Index.Quote(), + Constant(IsNullable), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update((SqlExpression)visitor.Visit(Array), (SqlExpression)visitor.Visit(Index)); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs index 3d03488f1..9c6d56fbe 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs @@ -8,6 +8,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgArraySliceExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The array being sliced. /// @@ -72,6 +74,18 @@ public virtual PgArraySliceExpression Update(SqlExpression array, SqlExpression? ? this : new PgArraySliceExpression(array, lowerBound, upperBound, IsNullable, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgArraySliceExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(SqlExpression), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Array.Quote(), + RelationalExpressionQuotingUtilities.VisitOrNull(LowerBound), + RelationalExpressionQuotingUtilities.VisitOrNull(UpperBound), + Constant(IsNullable), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override Expression VisitChildren(ExpressionVisitor visitor) => Update( diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs index 0cbee2c1e..03387b988 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs @@ -7,6 +7,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgBinaryExpression : SqlExpression { + private static ConstructorInfo? _quotingConstructor; + /// /// Creates a new instance of the class. /// @@ -74,6 +76,17 @@ public virtual PgBinaryExpression Update(SqlExpression left, SqlExpression right : this; } + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgBinaryExpression).GetConstructor( + [typeof(PgExpressionType), typeof(SqlExpression), typeof(SqlExpression), typeof(Type), typeof(RelationalTypeMapping)])!, + Constant(OperatorType), + Left.Quote(), + Right.Quote(), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs index 850b1f358..2304618ec 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs @@ -6,6 +6,8 @@ // ReSharper disable once InconsistentNaming public class PgILikeExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The match expression. /// @@ -60,6 +62,16 @@ public virtual PgILikeExpression Update( ? this : new PgILikeExpression(match, pattern, escapeChar, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgILikeExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(SqlExpression), typeof(RelationalTypeMapping)])!, + Match.Quote(), + Pattern.Quote(), + RelationalExpressionQuotingUtilities.VisitOrNull(EscapeChar), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public override bool Equals(object? obj) => obj is PgILikeExpression other && Equals(other); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs index 076c8caea..61871b567 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs @@ -5,6 +5,8 @@ /// public class PgJsonTraversalExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The match expression. /// @@ -56,6 +58,17 @@ public virtual PgJsonTraversalExpression Update(SqlExpression expression, IReadO ? this : new PgJsonTraversalExpression(expression, path, ReturnsText, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgJsonTraversalExpression).GetConstructor( + [typeof(SqlExpression), typeof(IReadOnlyList), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)])!, + Expression.Quote(), + NewArrayInit(typeof(SqlExpression), initializers: Path.Select(a => a.Quote())), + Constant(ReturnsText), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// /// Appends an additional path component to this and returns the result. /// diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs index 5aafba511..22cfe208d 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs @@ -5,6 +5,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgNewArrayExpression : SqlExpression { + private static ConstructorInfo? _quotingConstructor; + /// /// Creates a new instance of the class. /// @@ -74,6 +76,15 @@ public virtual PgNewArrayExpression Update(IReadOnlyList expressi : new PgNewArrayExpression(expressions, Type, TypeMapping); } + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgNewArrayExpression).GetConstructor( + [typeof(IReadOnlyList), typeof(Type), typeof(RelationalTypeMapping)])!, + NewArrayInit(typeof(SqlExpression), initializers: Expressions.Select(a => a.Quote())), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs index fa2a1a34b..f907e7b42 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs @@ -7,6 +7,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgRegexMatchExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// public override Type Type => typeof(bool); @@ -58,6 +60,16 @@ public virtual PgRegexMatchExpression Update(SqlExpression match, SqlExpression ? new PgRegexMatchExpression(match, pattern, Options, TypeMapping) : this; + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgRegexMatchExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(RegexOptions), typeof(RelationalTypeMapping)])!, + Match.Quote(), + Pattern.Quote(), + Constant(Options), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public virtual bool Equals(PgRegexMatchExpression? other) => ReferenceEquals(this, other) diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs index 19d8c6b22..0d0e6f305 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs @@ -11,13 +11,18 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// public class PgRowValueExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The values of this PostgreSQL row value expression. /// public virtual IReadOnlyList Values { get; } /// - public PgRowValueExpression(IReadOnlyList values, Type type, RelationalTypeMapping? typeMapping = null) + public PgRowValueExpression( + IReadOnlyList values, + Type type, + RelationalTypeMapping? typeMapping = null) : base(type, typeMapping) { Check.NotNull(values, nameof(values)); @@ -64,6 +69,15 @@ public virtual PgRowValueExpression Update(IReadOnlyList values) ? this : new PgRowValueExpression(values, Type); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgRowValueExpression).GetConstructor( + [typeof(IReadOnlyList), typeof(Type), typeof(RelationalTypeMapping)])!, + NewArrayInit(typeof(SqlExpression), initializers: Values.Select(a => a.Quote())), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// protected override void Print(ExpressionPrinter expressionPrinter) { diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs index bf2b89126..2d8dd0ee7 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs @@ -9,6 +9,8 @@ /// public class PgUnknownBinaryExpression : SqlExpression, IEquatable { + private static ConstructorInfo? _quotingConstructor; + /// /// The left-hand expression. /// @@ -59,6 +61,17 @@ public virtual PgUnknownBinaryExpression Update(SqlExpression left, SqlExpressio ? this : new PgUnknownBinaryExpression(left, right, Operator, Type, TypeMapping); + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(PgUnknownBinaryExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(string), typeof(Type), typeof(RelationalTypeMapping)])!, + Left.Quote(), + Right.Quote(), + Constant(Operator), + Constant(Type), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + /// public virtual bool Equals(PgUnknownBinaryExpression? other) => ReferenceEquals(this, other) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 3afb677bf..5eba3759a 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -1088,8 +1088,7 @@ void GenerateJsonPath(bool returnsText) s => s switch { { PropertyName: string propertyName } - => new SqlConstantExpression( - Expression.Constant(propertyName), _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), + => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), { ArrayIndex: SqlExpression arrayIndex } => arrayIndex, _ => throw new UnreachableException() }).ToList()); diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs index af2cb57ea..0acc8a795 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs @@ -40,6 +40,15 @@ public override Expression Process(Expression query) return result; } + /// + /// 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. + /// + protected override Expression ProcessTypeMappings(Expression expression) + => new NpgsqlTypeMappingPostprocessor(Dependencies, RelationalDependencies, RelationalQueryCompilationContext).Process(expression); + /// /// 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 diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs index 84ec8668a..0c4f030cb 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs @@ -266,18 +266,6 @@ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityT .SelectMany(t => t.GetDeclaredNavigations()); } - /// - /// 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. - /// - protected override Expression ApplyInferredTypeMappings( - Expression expression, - IReadOnlyDictionary<(string, string), RelationalTypeMapping?> inferredTypeMappings) - => new NpgsqlInferredTypeMappingApplier( - RelationalDependencies.Model, _typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression); - /// /// 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 @@ -1310,62 +1298,4 @@ public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) return base.Visit(expression); } } - - /// - /// 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. - /// - protected class NpgsqlInferredTypeMappingApplier : RelationalInferredTypeMappingApplier - { - private readonly NpgsqlTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - /// - /// 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. - /// - public NpgsqlInferredTypeMappingApplier( - IModel model, - NpgsqlTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IReadOnlyDictionary<(string, string), RelationalTypeMapping?> inferredTypeMappings) - : base(model, sqlExpressionFactory, inferredTypeMappings) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - /// 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. - /// - protected override Expression VisitExtension(Expression expression) - { - switch (expression) - { - case PgUnnestExpression unnestExpression - when TryGetInferredTypeMapping(unnestExpression.Alias, unnestExpression.ColumnName, out var elementTypeMapping): - { - var collectionTypeMapping = _typeMappingSource.FindMapping(unnestExpression.Array.Type, Model, elementTypeMapping); - - if (collectionTypeMapping is null) - { - throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(expression.Print())); - } - - return unnestExpression.Update( - _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping)); - } - - default: - return base.VisitExtension(expression); - } - } - } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs index 056e73f0c..c309220f8 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs @@ -261,7 +261,7 @@ protected virtual SqlExpression VisitAny(PgAnyExpression anyExpression, bool all _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function( "array_position", - new[] { array, _sqlExpressionFactory.Constant(null, item.TypeMapping) }, + new[] { array, _sqlExpressionFactory.Constant(null, item.Type, item.TypeMapping) }, nullable: true, argumentsPropagateNullability: FalseArrays[2], typeof(int))))); diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index e6194f099..6234f7097 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -411,7 +411,9 @@ private bool TryTranslateStartsEndsWithContains( // simple LIKE translation = patternConstant.Value switch { - null => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant(null, stringTypeMapping)), + null => _sqlExpressionFactory.Like( + translatedInstance, + _sqlExpressionFactory.Constant(null, typeof(string), stringTypeMapping)), // In .NET, all strings start with/end with/contain the empty string, but SQL LIKE return false for empty patterns. // Return % which always matches instead. diff --git a/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs new file mode 100644 index 000000000..ba50c0577 --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs @@ -0,0 +1,60 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +/// +/// 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. +/// +public class NpgsqlTypeMappingPostprocessor : RelationalTypeMappingPostprocessor +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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. + /// + public NpgsqlTypeMappingPostprocessor( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + RelationalQueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + _typeMappingSource = relationalDependencies.TypeMappingSource; + _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; + } + + /// + /// 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. + /// + protected override Expression VisitExtension(Expression expression) + { + switch (expression) + { + case PgUnnestExpression unnestExpression + when TryGetInferredTypeMapping(unnestExpression.Alias, unnestExpression.ColumnName, out var elementTypeMapping): + { + var collectionTypeMapping = _typeMappingSource.FindMapping(unnestExpression.Array.Type, Model, elementTypeMapping); + + if (collectionTypeMapping is null) + { + throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(expression.Print())); + } + + return unnestExpression.Update( + _sqlExpressionFactory.ApplyTypeMapping(unnestExpression.Array, collectionTypeMapping)); + } + + default: + return base.VisitExtension(expression); + } + } +} diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 91d898672..368ddc238 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -540,7 +540,7 @@ bool TryGetRowValueValues(SqlExpression e, [NotNullWhen(true)] out IReadOnlyList for (var i = 0; i < v.Length; i++) { - v[i] = Constant(constantTuple[i]); + v[i] = Constant(constantTuple[i], typeof(object)); } values = v; @@ -651,7 +651,7 @@ private SqlExpression ApplyTypeMappingOnArrayIndex( // If a (non-null) type mapping is being applied, it's to the element being indexed. // Infer the array's mapping from that. var (_, array) = typeMapping is not null - ? ApplyTypeMappingsOnItemAndArray(Constant(null, typeMapping), pgArrayIndexExpression.Array) + ? ApplyTypeMappingsOnItemAndArray(Constant(null, typeMapping.ClrType, typeMapping), pgArrayIndexExpression.Array) : (null, ApplyDefaultTypeMapping(pgArrayIndexExpression.Array)); return new PgArrayIndexExpression( diff --git a/test/Directory.Build.props b/test/Directory.Build.props index e4c47baca..8648a51f4 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,7 +3,7 @@ - net8.0 + net9.0 false false diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 190860809..e2cc6280d 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -3369,6 +3369,169 @@ await Test( #endregion PostgreSQL full-text search + [ConditionalFact] + public override async Task Add_required_primitive_collection_to_existing_table() + { + await base.Add_required_primitive_collection_to_existing_table(); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[]'; +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitive_collection_with_custom_default_value_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_default_value_to_existing_table(); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[1,2,3]'; +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitive_collection_with_custom_default_value_sql_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_default_value_sql_to_existing_table_core("N'[3, 2, 1]'"); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT (N'[3, 2, 1]'); +"""); + } + + [ConditionalFact(Skip = "issue #33038")] + public override async Task Add_required_primitive_collection_with_custom_converter_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_converter_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'nothing'; +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitive_collection_with_custom_converter_and_custom_default_value_to_existing_table() + { + await base.Add_required_primitive_collection_with_custom_converter_and_custom_default_value_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'some numbers'; +"""); + } + + [ConditionalFact] + public override async Task Add_optional_primitive_collection_to_existing_table() + { + await base.Add_optional_primitive_collection_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NULL; +"""); + } + + [ConditionalFact] + public override async Task Create_table_with_required_primitive_collection() + { + await base.Create_table_with_required_primitive_collection(); + + AssertSql( +""" +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [Numbers] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public override async Task Create_table_with_optional_primitive_collection() + { + await base.Create_table_with_optional_primitive_collection(); + + AssertSql( +""" +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [Numbers] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public override async Task Create_table_with_complex_type_with_required_properties_on_derived_entity_in_TPH() + { + await base.Create_table_with_complex_type_with_required_properties_on_derived_entity_in_TPH(); + + AssertSql( +""" +CREATE TABLE [Contacts] ( + [Id] int NOT NULL IDENTITY, + [Discriminator] nvarchar(8) NOT NULL, + [Name] nvarchar(max) NULL, + [Number] int NULL, + [MyComplex_Prop] nvarchar(max) NULL, + [MyComplex_MyNestedComplex_Bar] datetime2 NULL, + [MyComplex_MyNestedComplex_Foo] int NULL, + CONSTRAINT [PK_Contacts] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitve_collection_to_existing_table() + { + await base.Add_required_primitve_collection_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[]'; +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitve_collection_with_custom_default_value_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_default_value_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[1,2,3]'; +"""); + } + + [ConditionalFact(Skip = "issue #33038")] + public override async Task Add_required_primitve_collection_with_custom_converter_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_converter_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'nothing'; +"""); + } + + [ConditionalFact] + public override async Task Add_required_primitve_collection_with_custom_converter_and_custom_default_value_to_existing_table() + { + await base.Add_required_primitve_collection_with_custom_converter_and_custom_default_value_to_existing_table(); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'some numbers'; +"""); + } + + protected override string NonDefaultCollation => "POSIX"; diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs index 84b0e0a29..c6acdf2f5 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs @@ -4,10 +4,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +#nullable enable + public class NpgsqlTestStore : RelationalTestStore { - private readonly string _scriptPath; - private readonly string _additionalSql; + private readonly string? _scriptPath; + private readonly string? _additionalSql; private const string Northwind = "Northwind"; @@ -17,55 +19,53 @@ public class NpgsqlTestStore : RelationalTestStore public static NpgsqlTestStore GetNorthwindStore() => (NpgsqlTestStore)NpgsqlNorthwindTestStoreFactory.Instance - .GetOrCreate(NpgsqlNorthwindTestStoreFactory.Name).Initialize(null, (Func)null); + .GetOrCreate(NpgsqlNorthwindTestStoreFactory.Name).Initialize(null, (Func?)null); // ReSharper disable once UnusedMember.Global public static NpgsqlTestStore GetOrCreateInitialized(string name) - => new NpgsqlTestStore(name).InitializeNpgsql(null, (Func)null, null); + => new NpgsqlTestStore(name).InitializeNpgsql(null, (Func?)null, null); public static NpgsqlTestStore GetOrCreate( string name, - string scriptPath = null, - string additionalSql = null, - string connectionStringOptions = null) + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null) => new(name, scriptPath, additionalSql, connectionStringOptions); - public static NpgsqlTestStore Create(string name, string connectionStringOptions = null) + public static NpgsqlTestStore Create(string name, string? connectionStringOptions = null) => new(name, connectionStringOptions: connectionStringOptions, shared: false); public static NpgsqlTestStore CreateInitialized(string name) => new NpgsqlTestStore(name, shared: false) - .InitializeNpgsql(null, (Func)null, null); + .InitializeNpgsql(null, (Func?)null, null); private NpgsqlTestStore( string name, - string scriptPath = null, - string additionalSql = null, - string connectionStringOptions = null, + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, bool shared = true) - : base(name, shared) + : base(name, shared, CreateConnection(name, connectionStringOptions)) { Name = name; if (scriptPath is not null) { // ReSharper disable once AssignNullToNotNullAttribute - _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(NpgsqlTestStore).GetTypeInfo().Assembly.Location), scriptPath); + _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(NpgsqlTestStore).GetTypeInfo().Assembly.Location)!, scriptPath); } _additionalSql = additionalSql; - - // ReSharper disable VirtualMemberCallInConstructor - ConnectionString = CreateConnectionString(Name, connectionStringOptions); - Connection = new NpgsqlConnection(ConnectionString); - // ReSharper restore VirtualMemberCallInConstructor } + private static NpgsqlConnection CreateConnection(string name, string? connectionStringOptions) + => new(CreateConnectionString(name, connectionStringOptions)); + // ReSharper disable once MemberCanBePrivate.Global public NpgsqlTestStore InitializeNpgsql( - IServiceProvider serviceProvider, - Func createContext, - Action seed) + IServiceProvider? serviceProvider, + Func? createContext, + Action? seed) => (NpgsqlTestStore)Initialize(serviceProvider, createContext, seed); // ReSharper disable once UnusedMember.Global @@ -75,7 +75,7 @@ public NpgsqlTestStore InitializeNpgsql( Action seed) => InitializeNpgsql(serviceProvider, () => createContext(this), seed); - protected override void Initialize(Func createContext, Action seed, Action clean) + protected override void Initialize(Func createContext, Action? seed, Action? clean) { if (CreateDatabase(clean)) { @@ -123,7 +123,7 @@ private static string GetScratchDbName() return name; } - private bool CreateDatabase(Action clean) + private bool CreateDatabase(Action? clean) { using (var master = new NpgsqlConnection(CreateAdminConnectionString())) { @@ -273,34 +273,34 @@ public T ExecuteScalar(string sql, params object[] parameters) => ExecuteScalar(Connection, sql, parameters); private static T ExecuteScalar(DbConnection connection, string sql, params object[] parameters) - => Execute(connection, command => (T)command.ExecuteScalar(), sql, false, parameters); + => Execute(connection, command => (T)command.ExecuteScalar()!, sql, false, parameters); // ReSharper disable once UnusedMember.Global public Task ExecuteScalarAsync(string sql, params object[] parameters) => ExecuteScalarAsync(Connection, sql, parameters); - private static Task ExecuteScalarAsync(DbConnection connection, string sql, object[] parameters = null) - => ExecuteAsync(connection, async command => (T)await command.ExecuteScalarAsync(), sql, false, parameters); + private static Task ExecuteScalarAsync(DbConnection connection, string sql, object[]? parameters = null) + => ExecuteAsync(connection, async command => (T)(await command.ExecuteScalarAsync())!, sql, false, parameters); // ReSharper disable once UnusedMethodReturnValue.Global public int ExecuteNonQuery(string sql, params object[] parameters) => ExecuteNonQuery(Connection, sql, parameters); - private static int ExecuteNonQuery(DbConnection connection, string sql, object[] parameters = null) + private static int ExecuteNonQuery(DbConnection connection, string sql, object[]? parameters = null) => Execute(connection, command => command.ExecuteNonQuery(), sql, false, parameters); // ReSharper disable once UnusedMember.Global public Task ExecuteNonQueryAsync(string sql, params object[] parameters) => ExecuteNonQueryAsync(Connection, sql, parameters); - private static Task ExecuteNonQueryAsync(DbConnection connection, string sql, object[] parameters = null) + private static Task ExecuteNonQueryAsync(DbConnection connection, string sql, object[]? parameters = null) => ExecuteAsync(connection, command => command.ExecuteNonQueryAsync(), sql, false, parameters); // ReSharper disable once UnusedMember.Global public IEnumerable Query(string sql, params object[] parameters) => Query(Connection, sql, parameters); - private static IEnumerable Query(DbConnection connection, string sql, object[] parameters = null) + private static IEnumerable Query(DbConnection connection, string sql, object[]? parameters = null) => Execute( connection, command => { @@ -320,7 +320,7 @@ private static IEnumerable Query(DbConnection connection, string sql, obje public Task> QueryAsync(string sql, params object[] parameters) => QueryAsync(Connection, sql, parameters); - private static Task> QueryAsync(DbConnection connection, string sql, object[] parameters = null) + private static Task> QueryAsync(DbConnection connection, string sql, object[]? parameters = null) => ExecuteAsync( connection, async command => { @@ -341,7 +341,7 @@ private static T Execute( Func execute, string sql, bool useTransaction = false, - object[] parameters = null) + object[]? parameters = null) => ExecuteCommand(connection, execute, sql, useTransaction, parameters); private static T ExecuteCommand( @@ -349,7 +349,7 @@ private static T ExecuteCommand( Func execute, string sql, bool useTransaction, - object[] parameters) + object[]? parameters) { if (connection.State != ConnectionState.Closed) { @@ -388,7 +388,7 @@ private static Task ExecuteAsync( Func> executeAsync, string sql, bool useTransaction = false, - IReadOnlyList parameters = null) + IReadOnlyList? parameters = null) => ExecuteCommandAsync(connection, executeAsync, sql, useTransaction, parameters); private static async Task ExecuteCommandAsync( @@ -396,7 +396,7 @@ private static async Task ExecuteCommandAsync( Func> executeAsync, string sql, bool useTransaction, - IReadOnlyList parameters) + IReadOnlyList? parameters) { if (connection.State != ConnectionState.Closed) { @@ -432,7 +432,7 @@ private static async Task ExecuteCommandAsync( private static DbCommand CreateCommand( DbConnection connection, string commandText, - IReadOnlyList parameters = null) + IReadOnlyList? parameters = null) { var command = (NpgsqlCommand)connection.CreateCommand(); @@ -450,7 +450,7 @@ private static DbCommand CreateCommand( return command; } - public static string CreateConnectionString(string name, string options = null) + public static string CreateConnectionString(string name, string? options = null) { var builder = new NpgsqlConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = name };