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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6eed22bfd..e4a10226f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,7 +27,7 @@ on: - cron: '30 22 * * 6' env: - dotnet_sdk_version: '8.0.100' + dotnet_sdk_version: '9.0.100-preview.3.24204.13' jobs: analyze: 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..0fb184004 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,12 @@ - - + + + + + + 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/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index d842ad664..421d004c8 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 190860809..2277793c9 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -391,40 +391,35 @@ public override async Task Drop_table() { await base.Drop_table(); - AssertSql( - @"DROP TABLE ""People"";"); + AssertSql("""DROP TABLE "People";"""); } public override async Task Alter_table_add_comment() { await base.Alter_table_add_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS 'Table comment';"); + AssertSql("""COMMENT ON TABLE "People" IS 'Table comment';"""); } public override async Task Alter_table_add_comment_non_default_schema() { await base.Alter_table_add_comment_non_default_schema(); - AssertSql( - @"COMMENT ON TABLE ""SomeOtherSchema"".""People"" IS 'Table comment';"); + AssertSql("""COMMENT ON TABLE "SomeOtherSchema"."People" IS 'Table comment';"""); } public override async Task Alter_table_change_comment() { await base.Alter_table_change_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS 'Table comment2';"); + AssertSql("""COMMENT ON TABLE "People" IS 'Table comment2';"""); } public override async Task Alter_table_remove_comment() { await base.Alter_table_remove_comment(); - AssertSql( - @"COMMENT ON TABLE ""People"" IS NULL;"); + AssertSql("""COMMENT ON TABLE "People" IS NULL;"""); } [Fact] @@ -478,8 +473,7 @@ await Test( builder => builder.Entity("People").IsUnlogged(), asserter: null); // We don't scaffold unlogged - AssertSql( - @"ALTER TABLE ""People"" SET UNLOGGED;"); + AssertSql("""ALTER TABLE "People" SET UNLOGGED;"""); } [Fact] @@ -496,8 +490,7 @@ await Test( _ => { }, asserter: null); // We don't scaffold unlogged - AssertSql( - @"ALTER TABLE ""People"" SET LOGGED;"); + AssertSql("""ALTER TABLE "People" SET LOGGED;"""); } public override async Task Rename_table() @@ -505,11 +498,11 @@ public override async Task Rename_table() await base.Rename_table(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" RENAME TO ""Persons"";", + """ALTER TABLE "People" RENAME TO "Persons";""", // - @"ALTER TABLE ""Persons"" ADD CONSTRAINT ""PK_Persons"" PRIMARY KEY (""Id"");"); + """ALTER TABLE "Persons" ADD CONSTRAINT "PK_Persons" PRIMARY KEY ("Id");"""); } public override async Task Rename_table_with_primary_key() @@ -517,11 +510,11 @@ public override async Task Rename_table_with_primary_key() await base.Rename_table_with_primary_key(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" RENAME TO ""Persons"";", + """ALTER TABLE "People" RENAME TO "Persons";""", // - @"ALTER TABLE ""Persons"" ADD CONSTRAINT ""PK_Persons"" PRIMARY KEY (""Id"");"); + """ALTER TABLE "Persons" ADD CONSTRAINT "PK_Persons" PRIMARY KEY ("Id");"""); } public override async Task Move_table() @@ -594,8 +587,7 @@ public override async Task Add_column_with_defaultValue_string() { await base.Add_column_with_defaultValue_string(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text NOT NULL DEFAULT 'John Doe';"); + AssertSql("""ALTER TABLE "People" ADD "Name" text NOT NULL DEFAULT 'John Doe';"""); } public override async Task Add_column_with_defaultValue_datetime() @@ -614,8 +606,7 @@ await Test( Assert.False(birthdayColumn.IsNullable); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Birthday"" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12T17:05:00Z';"); + AssertSql("""ALTER TABLE "People" ADD "Birthday" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12T17:05:00Z';"""); } [Fact] @@ -623,8 +614,7 @@ public override async Task Add_column_with_defaultValueSql() { await base.Add_column_with_defaultValueSql(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Sum"" integer NOT NULL DEFAULT (1 + 2);"); + AssertSql("""ALTER TABLE "People" ADD "Sum" integer NOT NULL DEFAULT (1 + 2);"""); } public override async Task Add_column_with_computedSql(bool? stored) @@ -644,40 +634,35 @@ public override async Task Add_column_with_computedSql(bool? stored) await base.Add_column_with_computedSql(stored: true); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Sum"" text GENERATED ALWAYS AS (""X"" + ""Y"") STORED;"); + AssertSql("""ALTER TABLE "People" ADD "Sum" text GENERATED ALWAYS AS ("X" + "Y") STORED;"""); } public override async Task Add_column_with_required() { await base.Add_column_with_required(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text NOT NULL DEFAULT '';"); + AssertSql("""ALTER TABLE "People" ADD "Name" text NOT NULL DEFAULT '';"""); } public override async Task Add_column_with_ansi() { await base.Add_column_with_ansi(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } public override async Task Add_column_with_max_length() { await base.Add_column_with_max_length(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" character varying(30);"); + AssertSql("""ALTER TABLE "People" ADD "Name" character varying(30);"""); } public override async Task Add_column_with_unbounded_max_length() { await base.Add_column_with_unbounded_max_length(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } public override async Task Add_column_with_max_length_on_derived() @@ -691,7 +676,7 @@ public override async Task Add_column_with_fixed_length() { await base.Add_column_with_fixed_length(); - AssertSql(@"ALTER TABLE ""People"" ADD ""Name"" character(100);"); + AssertSql("""ALTER TABLE "People" ADD "Name" character(100);"""); } public override async Task Add_column_with_comment() @@ -709,8 +694,7 @@ public override async Task Add_column_with_collation() { await base.Add_column_with_collation(); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "POSIX";"""); } public override async Task Add_column_computed_with_collation(bool stored) @@ -730,8 +714,7 @@ public override async Task Add_column_computed_with_collation(bool stored) await base.Add_column_computed_with_collation(stored); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"" GENERATED ALWAYS AS ('hello') STORED;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "POSIX" GENERATED ALWAYS AS ('hello') STORED;"""); } #pragma warning disable CS0618 @@ -754,8 +737,7 @@ await Test( Assert.Equal("POSIX", nameColumn.Collation); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "POSIX";"""); } [ConditionalFact] @@ -778,8 +760,7 @@ await Test( Assert.Equal("C", nameColumn.Collation); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text COLLATE ""C"";"); + AssertSql("""ALTER TABLE "People" ADD "Name" text COLLATE "C";"""); } #pragma warning restore CS0618 @@ -834,8 +815,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); } [Fact] @@ -859,8 +839,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED ALWAYS AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED ALWAYS AS IDENTITY;"""); } [Fact] @@ -898,7 +877,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10);"); + """ +ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10); +"""); } [Fact] @@ -935,8 +916,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" serial NOT NULL;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" serial NOT NULL;"""); } [Fact] @@ -960,8 +940,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""SomeColumn"" integer GENERATED BY DEFAULT AS IDENTITY;"); + AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); } [Fact] @@ -1008,8 +987,7 @@ await Test( Assert.Equal("text", column.StoreType); }); - AssertSql( - @"ALTER TABLE ""People"" ADD ""Name"" text;"); + AssertSql("""ALTER TABLE "People" ADD "Name" text;"""); } [Fact] @@ -1031,20 +1009,14 @@ await Test( Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - """ -ALTER TABLE "Blogs" ADD "Title" text COMPRESSION pglz; -"""); + AssertSql("""ALTER TABLE "Blogs" ADD "Title" text COMPRESSION pglz;"""); } public override async Task Alter_column_change_type() { await base.Alter_column_change_type(); - AssertSql( - """ -ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE bigint; -"""); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "SomeColumn" TYPE bigint;"""); } public override async Task Alter_column_make_required() @@ -1113,9 +1085,9 @@ public override async Task Alter_column_make_computed(bool? stored) await base.Alter_column_make_computed(stored); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" + ""Y"") STORED NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" + "Y") STORED NOT NULL;"""); } public override async Task Alter_column_change_computed() @@ -1150,9 +1122,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" - ""Y"") STORED NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" - "Y") STORED NOT NULL;"""); } public override async Task Alter_column_change_computed_recreates_indexes() @@ -1195,11 +1167,11 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer GENERATED ALWAYS AS (""X"" - ""Y"") STORED NOT NULL;", + """ALTER TABLE "People" ADD "Sum" integer GENERATED ALWAYS AS ("X" - "Y") STORED NOT NULL;""", // - @"CREATE INDEX ""IX_People_Sum"" ON ""People"" (""Sum"");"); + """CREATE INDEX "IX_People_Sum" ON "People" ("Sum");"""); } public override Task Alter_column_change_computed_type() @@ -1233,17 +1205,16 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Sum"";", + """ALTER TABLE "People" DROP COLUMN "Sum";""", // - @"ALTER TABLE ""People"" ADD ""Sum"" integer NOT NULL;"); + """ALTER TABLE "People" ADD "Sum" integer NOT NULL;"""); } public override async Task Alter_column_add_comment() { await base.Alter_column_add_comment(); - AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS 'Some comment';"); + AssertSql("""COMMENT ON COLUMN "People"."Id" IS 'Some comment';"""); } public override async Task Alter_computed_column_add_comment() @@ -1273,16 +1244,14 @@ await Test( } }); - AssertSql( - @"COMMENT ON COLUMN ""People"".""SomeColumn"" IS 'Some comment';"); + AssertSql("""COMMENT ON COLUMN "People"."SomeColumn" IS 'Some comment';"""); } public override async Task Alter_column_change_comment() { await base.Alter_column_change_comment(); - AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS 'Some comment2';"); + AssertSql("""COMMENT ON COLUMN "People"."Id" IS 'Some comment2';"""); } public override async Task Alter_column_remove_comment() @@ -1290,7 +1259,7 @@ public override async Task Alter_column_remove_comment() await base.Alter_column_remove_comment(); AssertSql( - @"COMMENT ON COLUMN ""People"".""Id"" IS NULL;"); + """COMMENT ON COLUMN "People"."Id" IS NULL;"""); } [Fact] @@ -1337,8 +1306,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET GENERATED ALWAYS;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" SET GENERATED ALWAYS;"""); } [Fact] @@ -1610,7 +1578,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" SET GENERATED ALWAYS;"); + """ALTER TABLE "People" ALTER COLUMN "Id" SET GENERATED ALWAYS;"""); } [Fact] @@ -1668,8 +1636,7 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" TYPE bigint;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" TYPE bigint;"""); } [Fact] @@ -1693,8 +1660,7 @@ await Test( Assert.Equal(10, options.StartValue); // Restarting doesn't change the scaffolded start value }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Id"" RESTART WITH 20;"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" RESTART WITH 20;"""); } [Fact] @@ -1702,8 +1668,7 @@ public override async Task Alter_column_set_collation() { await base.Alter_column_set_collation(); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name" TYPE text COLLATE "POSIX";"""); } [Fact] @@ -1711,8 +1676,7 @@ public override async Task Alter_column_reset_collation() { await base.Alter_column_reset_collation(); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""default"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name" TYPE text COLLATE "default";"""); } public override async Task Convert_string_column_to_a_json_column_containing_reference() @@ -1759,8 +1723,7 @@ await Test( // Assert.Equal("C", nameColumn.Collation); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name"" TYPE text COLLATE ""C"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name" TYPE text COLLATE "C";"""); } #pragma warning restore CS0618 @@ -1789,8 +1752,7 @@ await Test( Assert.Equal(NonDefaultCollation, computedColumn.Collation); }); - AssertSql( - @"ALTER TABLE ""People"" ALTER COLUMN ""Name2"" TYPE text COLLATE ""POSIX"";"); + AssertSql("""ALTER TABLE "People" ALTER COLUMN "Name2" TYPE text COLLATE "POSIX";"""); } [Fact] @@ -1812,8 +1774,7 @@ await Test( Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ALTER COLUMN ""Title"" SET COMPRESSION pglz"); + AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION pglz"""); } [Fact] @@ -1835,16 +1796,14 @@ await Test( Assert.Null(column[NpgsqlAnnotationNames.CompressionMethod]); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ALTER COLUMN ""Title"" SET COMPRESSION default"); + AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION default"""); } public override async Task Drop_column() { await base.Drop_column(); - AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""SomeColumn"";"); + AssertSql("""ALTER TABLE "People" DROP COLUMN "SomeColumn";"""); } public override async Task Drop_column_primary_key() @@ -1852,9 +1811,9 @@ public override async Task Drop_column_primary_key() await base.Drop_column_primary_key(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" DROP COLUMN ""Id"";"); + """ALTER TABLE "People" DROP COLUMN "Id";"""); } public override async Task Drop_column_computed_and_non_computed_with_dependency() @@ -1880,9 +1839,9 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" DROP COLUMN ""Y"";", + """ALTER TABLE "People" DROP COLUMN "Y";""", // - @"ALTER TABLE ""People"" DROP COLUMN ""X"";"); + """ALTER TABLE "People" DROP COLUMN "X";"""); } [Fact] @@ -1918,8 +1877,7 @@ public override async Task Rename_column() { await base.Rename_column(); - AssertSql( - @"ALTER TABLE ""People"" RENAME COLUMN ""SomeColumn"" TO ""SomeOtherColumn"";"); + AssertSql("""ALTER TABLE "People" RENAME COLUMN "SomeColumn" TO "SomeOtherColumn";"""); } #endregion @@ -1930,24 +1888,21 @@ public override async Task Create_index_unique() { await base.Create_index_unique(); - AssertSql( - @"CREATE UNIQUE INDEX ""IX_People_FirstName_LastName"" ON ""People"" (""FirstName"", ""LastName"");"); + AssertSql("""CREATE UNIQUE INDEX "IX_People_FirstName_LastName" ON "People" ("FirstName", "LastName");"""); } public override async Task Create_index_descending() { await base.Create_index_descending(); - AssertSql( - @"CREATE INDEX ""IX_People_X"" ON ""People"" (""X"" DESC);"); + AssertSql("""CREATE INDEX "IX_People_X" ON "People" ("X" DESC);"""); } public override async Task Create_index_descending_mixed() { await base.Create_index_descending_mixed(); - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + AssertSql("""CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } #pragma warning disable CS0618 // HasSortOrder is obsolete @@ -1974,8 +1929,7 @@ await Test( Assert.Collection(index.IsDescending, Assert.False, Assert.True, Assert.False); }); - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + AssertSql("""CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } #pragma warning restore CS0618 @@ -2002,24 +1956,21 @@ await Test( Assert.Collection(index.IsDescending, Assert.False, Assert.True, Assert.False); }); - AssertSql( - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + AssertSql("""CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } public override async Task Create_index_with_filter() { await base.Create_index_with_filter(); - AssertSql( - @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } public override async Task Create_unique_index_with_filter() { await base.Create_unique_index_with_filter(); - AssertSql( - @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL AND ""Name"" <> '';"); + AssertSql("""CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL AND "Name" <> '';"""); } [Fact] @@ -2061,8 +2012,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", last_name);" - : @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"");"); + ? """CREATE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", last_name);""" + : """CREATE INDEX "IX_People_Name" ON "People" ("Name");"""); } [Fact] @@ -2106,8 +2057,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"") WHERE ""Name"" IS NOT NULL;" - : @"CREATE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + ? """CREATE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName") WHERE "Name" IS NOT NULL;""" + : """CREATE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } [Fact] @@ -2151,8 +2102,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"");" - : @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"");"); + ? """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName");""" + : """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name");"""); } [Fact] @@ -2198,8 +2149,8 @@ await Test( AssertSql( TestEnvironment.PostgresVersion.AtLeast(11) - ? @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") INCLUDE (""FirstName"", ""LastName"") WHERE ""Name"" IS NOT NULL;" - : @"CREATE UNIQUE INDEX ""IX_People_Name"" ON ""People"" (""Name"") WHERE ""Name"" IS NOT NULL;"); + ? """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") INCLUDE ("FirstName", "LastName") WHERE "Name" IS NOT NULL;""" + : """CREATE UNIQUE INDEX "IX_People_Name" ON "People" ("Name") WHERE "Name" IS NOT NULL;"""); } [Fact] @@ -2217,8 +2168,7 @@ await Test( .IsCreatedConcurrently(), asserter: null); // No scaffolding for IsCreatedConcurrently - AssertSql( - @"CREATE INDEX CONCURRENTLY ""IX_People_Age"" ON ""People"" (""Age"");"); + AssertSql("""CREATE INDEX CONCURRENTLY "IX_People_Age" ON "People" ("Age");"""); } [Fact] @@ -2241,8 +2191,7 @@ await Test( Assert.Equal("hash", index[NpgsqlAnnotationNames.IndexMethod]); }); - AssertSql( - @"CREATE INDEX ""IX_People_Age"" ON ""People"" USING hash (""Age"");"); + AssertSql("""CREATE INDEX "IX_People_Age" ON "People" USING hash ("Age");"""); } [Fact] @@ -2266,8 +2215,7 @@ await Test( Assert.Equal(new[] { "text_pattern_ops", null }, index[NpgsqlAnnotationNames.IndexOperators]); }); - AssertSql( - @"CREATE INDEX ""IX_People_FirstName_LastName"" ON ""People"" (""FirstName"" text_pattern_ops, ""LastName"");"); + AssertSql("""CREATE INDEX "IX_People_FirstName_LastName" ON "People" ("FirstName" text_pattern_ops, "LastName");"""); } [Fact] @@ -2288,10 +2236,7 @@ await Test( Assert.Equal("some_collation", Assert.Single((IReadOnlyList)index[RelationalAnnotationNames.Collation]!)); }); - AssertSql( - """ -CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation); -"""); + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation);"""); } [Fact] // #3027 @@ -2315,10 +2260,7 @@ await Test( Assert.Equal("some_collation", Assert.Single((IReadOnlyList)index[RelationalAnnotationNames.Collation]!)); }); - AssertSql( - """ -CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation text_pattern_ops); -"""); + AssertSql("""CREATE INDEX "IX_People_Name" ON "People" ("Name" COLLATE some_collation text_pattern_ops);"""); } [Fact] @@ -2346,7 +2288,7 @@ await Test( }); AssertSql( - @"CREATE INDEX ""IX_People_FirstName_MiddleName_LastName"" ON ""People"" (""FirstName"" NULLS FIRST, ""MiddleName"", ""LastName"" NULLS LAST);"); + """CREATE INDEX "IX_People_FirstName_MiddleName_LastName" ON "People" ("FirstName" NULLS FIRST, "MiddleName", "LastName" NULLS LAST);"""); } [Fact] @@ -2366,7 +2308,7 @@ await Test( _ => { }); AssertSql( - @"CREATE INDEX ""IX_Blogs_Title_Description"" ON ""Blogs"" (to_tsvector('simple', ""Title"" || ' ' || coalesce(""Description"", '')));"); + """CREATE INDEX "IX_Blogs_Title_Description" ON "Blogs" (to_tsvector('simple', "Title" || ' ' || coalesce("Description", '')));"""); } [Fact] @@ -2387,7 +2329,7 @@ await Test( _ => { }); AssertSql( - @"CREATE INDEX ""IX_Blogs_Title_Description"" ON ""Blogs"" USING GIN (to_tsvector('simple', ""Title"" || ' ' || coalesce(""Description"", '')));"); + """CREATE INDEX "IX_Blogs_Title_Description" ON "Blogs" USING GIN (to_tsvector('simple', "Title" || ' ' || coalesce("Description", '')));"""); } [ConditionalFact] @@ -2425,13 +2367,9 @@ await Test( }); AssertSql( - """ -CREATE UNIQUE INDEX "IX_NullsDistinct" ON "People" ("Age"); -""", + """CREATE UNIQUE INDEX "IX_NullsDistinct" ON "People" ("Age");""", // - """ -CREATE UNIQUE INDEX "IX_NullsNotDistinct" ON "People" ("Age") NULLS NOT DISTINCT; -"""); + """CREATE UNIQUE INDEX "IX_NullsNotDistinct" ON "People" ("Age") NULLS NOT DISTINCT;"""); } [Fact] @@ -2460,8 +2398,7 @@ await Test( Assert.Equal("70", storageParameter.Value); }); - AssertSql( - @"CREATE INDEX ""IX_People_Age"" ON ""People"" (""Age"") WITH (fillfactor=70);"); + AssertSql("""CREATE INDEX "IX_People_Age" ON "People" ("Age") WITH (fillfactor=70);"""); } public override async Task Alter_index_change_sort_order() @@ -2469,25 +2406,23 @@ public override async Task Alter_index_change_sort_order() await base.Alter_index_change_sort_order(); AssertSql( - @"DROP INDEX ""IX_People_X_Y_Z"";", + """DROP INDEX "IX_People_X_Y_Z";""", // - @"CREATE INDEX ""IX_People_X_Y_Z"" ON ""People"" (""X"", ""Y"" DESC, ""Z"");"); + """CREATE INDEX "IX_People_X_Y_Z" ON "People" ("X", "Y" DESC, "Z");"""); } public override async Task Drop_index() { await base.Drop_index(); - AssertSql( - @"DROP INDEX ""IX_People_SomeField"";"); + AssertSql("""DROP INDEX "IX_People_SomeField";"""); } public override async Task Rename_index() { await base.Rename_index(); - AssertSql( - @"ALTER INDEX ""Foo"" RENAME TO foo;"); + AssertSql("""ALTER INDEX "Foo" RENAME TO foo;"""); } #endregion @@ -2504,14 +2439,16 @@ public override async Task Add_primary_key_int() ALTER TABLE "People" ALTER COLUMN "SomeField" ADD GENERATED BY DEFAULT AS IDENTITY; """, // - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_People"" PRIMARY KEY (""SomeField"");"); + """ +ALTER TABLE "People" ADD CONSTRAINT "PK_People" PRIMARY KEY ("SomeField"); +"""); } public override async Task Add_primary_key_string() { await base.Add_primary_key_string(); - AssertSql(@"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_People"" PRIMARY KEY (""SomeField"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "PK_People" PRIMARY KEY ("SomeField");"""); } public override async Task Add_primary_key_with_name() @@ -2525,15 +2462,16 @@ public override async Task Add_primary_key_with_name() ALTER TABLE "People" ALTER COLUMN "SomeField" SET DEFAULT ''; """, // - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_Foo"" PRIMARY KEY (""SomeField"");"); + """ +ALTER TABLE "People" ADD CONSTRAINT "PK_Foo" PRIMARY KEY ("SomeField"); +"""); } public override async Task Add_primary_key_composite_with_name() { await base.Add_primary_key_composite_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""PK_Foo"" PRIMARY KEY (""SomeField1"", ""SomeField2"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "PK_Foo" PRIMARY KEY ("SomeField1", "SomeField2");"""); } public override async Task Drop_primary_key_int() @@ -2541,17 +2479,16 @@ public override async Task Drop_primary_key_int() await base.Drop_primary_key_int(); AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";", + """ALTER TABLE "People" DROP CONSTRAINT "PK_People";""", // - @"ALTER TABLE ""People"" ALTER COLUMN ""SomeField"" DROP IDENTITY;"); + """ALTER TABLE "People" ALTER COLUMN "SomeField" DROP IDENTITY;"""); } public override async Task Drop_primary_key_string() { await base.Drop_primary_key_string(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""PK_People"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "PK_People";"""); } public override Task Add_foreign_key() @@ -2562,9 +2499,9 @@ public override async Task Add_foreign_key_with_name() await base.Add_foreign_key_with_name(); AssertSql( - @"CREATE INDEX ""IX_Orders_CustomerId"" ON ""Orders"" (""CustomerId"");", + """CREATE INDEX "IX_Orders_CustomerId" ON "Orders" ("CustomerId");""", // - @"ALTER TABLE ""Orders"" ADD CONSTRAINT ""FK_Foo"" FOREIGN KEY (""CustomerId"") REFERENCES ""Customers"" (""Id"") ON DELETE CASCADE;"); + """ALTER TABLE "Orders" ADD CONSTRAINT "FK_Foo" FOREIGN KEY ("CustomerId") REFERENCES "Customers" ("Id") ON DELETE CASCADE;"""); } public override async Task Drop_foreign_key() @@ -2572,49 +2509,44 @@ public override async Task Drop_foreign_key() await base.Drop_foreign_key(); AssertSql( - @"ALTER TABLE ""Orders"" DROP CONSTRAINT ""FK_Orders_Customers_CustomerId"";", + """ALTER TABLE "Orders" DROP CONSTRAINT "FK_Orders_Customers_CustomerId";""", // - @"DROP INDEX ""IX_Orders_CustomerId"";"); + """DROP INDEX "IX_Orders_CustomerId";"""); } public override async Task Add_unique_constraint() { await base.Add_unique_constraint(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""AK_People_AlternateKeyColumn"" UNIQUE (""AlternateKeyColumn"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "AK_People_AlternateKeyColumn" UNIQUE ("AlternateKeyColumn");"""); } public override async Task Add_unique_constraint_composite_with_name() { await base.Add_unique_constraint_composite_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""AK_Foo"" UNIQUE (""AlternateKeyColumn1"", ""AlternateKeyColumn2"");"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "AK_Foo" UNIQUE ("AlternateKeyColumn1", "AlternateKeyColumn2");"""); } public override async Task Drop_unique_constraint() { await base.Drop_unique_constraint(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""AK_People_AlternateKeyColumn"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "AK_People_AlternateKeyColumn";"""); } public override async Task Add_check_constraint_with_name() { await base.Add_check_constraint_with_name(); - AssertSql( - @"ALTER TABLE ""People"" ADD CONSTRAINT ""CK_People_Foo"" CHECK (""DriverLicense"" > 0);"); + AssertSql("""ALTER TABLE "People" ADD CONSTRAINT "CK_People_Foo" CHECK ("DriverLicense" > 0);"""); } public override async Task Drop_check_constraint() { await base.Drop_check_constraint(); - AssertSql( - @"ALTER TABLE ""People"" DROP CONSTRAINT ""CK_People_Foo"";"); + AssertSql("""ALTER TABLE "People" DROP CONSTRAINT "CK_People_Foo";"""); } #endregion @@ -2625,10 +2557,7 @@ public override async Task Create_sequence() { await base.Create_sequence(); - AssertSql( - """ -CREATE SEQUENCE "TestSequence" AS integer START WITH 1 INCREMENT BY 1 NO CYCLE; -"""); + AssertSql("""CREATE SEQUENCE "TestSequence" AS integer START WITH 1 INCREMENT BY 1 NO CYCLE;"""); } public override async Task Create_sequence_all_settings() @@ -2645,7 +2574,9 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'dbo2') THEN END $EF$; """, // - @"CREATE SEQUENCE dbo2.""TestSequence"" START WITH 3 INCREMENT BY 2 MINVALUE 2 MAXVALUE 916 CYCLE CACHE 20;"); + """ +CREATE SEQUENCE dbo2."TestSequence" START WITH 3 INCREMENT BY 2 MINVALUE 2 MAXVALUE 916 CYCLE CACHE 20; +"""); } public override async Task Create_sequence_nocache() @@ -2693,10 +2624,7 @@ await Test( Assert.Equal("smallint", sequence.StoreType); }); - AssertSql( - """ -CREATE SEQUENCE "TestSequence" AS smallint START WITH 1 INCREMENT BY 1 NO CYCLE; -"""); + AssertSql("""CREATE SEQUENCE "TestSequence" AS smallint START WITH 1 INCREMENT BY 1 NO CYCLE;"""); } [Fact] @@ -2719,32 +2647,28 @@ public override async Task Alter_sequence_increment_by() { await base.Alter_sequence_increment_by(); - AssertSql( - @"ALTER SEQUENCE foo INCREMENT BY 2 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"); + AssertSql("ALTER SEQUENCE foo INCREMENT BY 2 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"); } public override async Task Alter_sequence_default_cache_to_cache() { await base.Alter_sequence_default_cache_to_cache(); - AssertSql( - """ALTER SEQUENCE "Delta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 20;"""); + AssertSql("""ALTER SEQUENCE "Delta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 20;"""); } public override async Task Alter_sequence_default_cache_to_nocache() { await base.Alter_sequence_default_cache_to_nocache(); - AssertSql( - """ALTER SEQUENCE "Epsilon" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); + AssertSql("""ALTER SEQUENCE "Epsilon" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); } public override async Task Alter_sequence_cache_to_nocache() { await base.Alter_sequence_cache_to_nocache(); - AssertSql( - """ALTER SEQUENCE "Zeta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); + AssertSql("""ALTER SEQUENCE "Zeta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); } public override async Task Alter_sequence_cache_to_default_cache() @@ -2762,18 +2686,14 @@ await Test( Assert.Null(sequence.CacheSize); }); - AssertSql( - """ALTER SEQUENCE "Eta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); + AssertSql("""ALTER SEQUENCE "Eta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); } public override async Task Alter_sequence_nocache_to_cache() { await base.Alter_sequence_nocache_to_cache(); - AssertSql( - """ -ALTER SEQUENCE "Theta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 20; -"""); + AssertSql("""ALTER SEQUENCE "Theta" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 20;"""); } public override async Task Alter_sequence_nocache_to_default_cache() @@ -2791,8 +2711,7 @@ await Test( Assert.Null(sequence.CacheSize); }); - AssertSql( - """ALTER SEQUENCE "Iota" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); + AssertSql("""ALTER SEQUENCE "Iota" INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;"""); } public override async Task Alter_sequence_restart_with() @@ -2810,16 +2729,14 @@ public override async Task Drop_sequence() { await base.Drop_sequence(); - AssertSql( - @"DROP SEQUENCE ""TestSequence"";"); + AssertSql("""DROP SEQUENCE "TestSequence";"""); } public override async Task Rename_sequence() { await base.Rename_sequence(); - AssertSql( - @"ALTER SEQUENCE ""TestSequence"" RENAME TO testsequence;"); + AssertSql("""ALTER SEQUENCE "TestSequence" RENAME TO testsequence;"""); } public override async Task Move_sequence() @@ -2836,7 +2753,9 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'TestSequenceSchema') T END $EF$; """, // - @"ALTER SEQUENCE ""TestSequence"" SET SCHEMA ""TestSequenceSchema"";"); + """ +ALTER SEQUENCE "TestSequence" SET SCHEMA "TestSequenceSchema"; +"""); } #endregion @@ -2993,7 +2912,6 @@ SELECT setval( #endregion Data seeding - [ConditionalFact] public override async Task Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table() { await base.Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table_core("ARRAY[3, 2, 1]"); @@ -3016,8 +2934,7 @@ await Test( Assert.Equal("public", citext.Schema); }); - AssertSql( - @"CREATE EXTENSION IF NOT EXISTS citext;"); + AssertSql("CREATE EXTENSION IF NOT EXISTS citext;"); } [Fact] @@ -3065,8 +2982,7 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql( - @"CREATE TYPE ""Mood"" AS ENUM ('Happy', 'Sad');"); + AssertSql("""CREATE TYPE "Mood" AS ENUM ('Happy', 'Sad');"""); } [Fact] @@ -3096,7 +3012,9 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN END $EF$; """, // - @"CREATE TYPE some_schema.""Mood"" AS ENUM ('Happy', 'Sad');"); + """ +CREATE TYPE some_schema."Mood" AS ENUM ('Happy', 'Sad'); +"""); } [Fact] @@ -3107,8 +3025,7 @@ await Test( _ => { }, model => Assert.Empty(model.GetPostgresEnums())); - AssertSql( - @"DROP TYPE ""Mood"";"); + AssertSql("""DROP TYPE "Mood";"""); } [Fact] // #979 @@ -3120,8 +3037,7 @@ await Test( builder => builder.HasPostgresEnum("Enum2", ["X", "Y"]), model => Assert.Equal(2, model.GetPostgresEnums().Count())); - AssertSql( - @"CREATE TYPE ""Enum2"" AS ENUM ('X', 'Y');"); + AssertSql("""CREATE TYPE "Enum2" AS ENUM ('X', 'Y');"""); } [Fact] @@ -3140,8 +3056,7 @@ await Test( l => Assert.Equal("Angry", l)); }); - AssertSql( - @"ALTER TYPE ""Mood"" ADD VALUE 'Angry';"); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry';"""); } [Fact] @@ -3160,8 +3075,7 @@ await Test( l => Assert.Equal("Sad", l)); }); - AssertSql( - @"ALTER TYPE ""Mood"" ADD VALUE 'Angry' BEFORE 'Sad';"); + AssertSql("""ALTER TYPE "Mood" ADD VALUE 'Angry' BEFORE 'Sad';"""); } [Fact] @@ -3240,8 +3154,7 @@ await Test( _ => { }, model => Assert.Empty(PostgresCollation.GetCollations(model))); - AssertSql( - @"DROP COLLATION dummy;"); + AssertSql("""DROP COLLATION dummy;"""); } [Fact] @@ -3273,8 +3186,7 @@ await Test( Assert.Equal("tsvector", column.StoreType); }); - AssertSql( - @"ALTER TABLE ""Blogs"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""TextColumn"")) STORED;"); + AssertSql("""ALTER TABLE "Blogs" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (to_tsvector('english', "TextColumn")) STORED;"""); } [Fact] @@ -3298,7 +3210,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (jsonb_to_tsvector('english', ""JsonbColumn"", '""all""')) STORED;"); + """ALTER TABLE "People" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (jsonb_to_tsvector('english', "JsonbColumn", '"all"')) STORED;"""); } [Fact] @@ -3329,7 +3241,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""SearchColumn"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""RequiredTextColumn"" || ' ' || coalesce(""OptionalTextColumn"", '')) || jsonb_to_tsvector('english', ""RequiredJsonbColumn"", '""all""') || json_to_tsvector('english', coalesce(""OptionalJsonColumn"", '{}'), '""all""')) STORED;"); + """ALTER TABLE "People" ADD "SearchColumn" tsvector GENERATED ALWAYS AS (to_tsvector('english', "RequiredTextColumn" || ' ' || coalesce("OptionalTextColumn", '')) || jsonb_to_tsvector('english', "RequiredJsonbColumn", '"all"') || json_to_tsvector('english', coalesce("OptionalJsonColumn", '{}'), '"all"')) STORED;"""); } [Fact] @@ -3362,13 +3274,138 @@ await Test( }); AssertSql( - @"ALTER TABLE ""Blogs"" DROP COLUMN ""TsVector"";", + """ALTER TABLE "Blogs" DROP COLUMN "TsVector";""", // - @"ALTER TABLE ""Blogs"" ADD ""TsVector"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""Title"" || ' ' || coalesce(""Description"", ''))) STORED;"); + """ALTER TABLE "Blogs" ADD "TsVector" tsvector GENERATED ALWAYS AS (to_tsvector('english', "Title" || ' ' || coalesce("Description", ''))) STORED;"""); } #endregion PostgreSQL full-text search + 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" integer[] NOT NULL;"""); + } + + 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" integer[] NOT NULL DEFAULT ARRAY[1,2,3]::integer[];"""); + } + + 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("ARRAY[1,2,3]"); + + AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT (ARRAY[1,2,3]);"""); + } + + [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'; +"""); + } + + 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" text NOT NULL DEFAULT 'some numbers';"""); + } + + 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" integer[];"""); + } + + public override async Task Create_table_with_required_primitive_collection() + { + await base.Create_table_with_required_primitive_collection(); + + AssertSql( + """ +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text, + "Numbers" integer[] NOT NULL, + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +"""); + } + + public override async Task Create_table_with_optional_primitive_collection() + { + await base.Create_table_with_optional_primitive_collection(); + + AssertSql( + """ +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text, + "Numbers" integer[], + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +"""); + } + + 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" integer GENERATED BY DEFAULT AS IDENTITY, + "Discriminator" character varying(8) NOT NULL, + "Name" text, + "Number" integer, + "MyComplex_Prop" text, + "MyComplex_MyNestedComplex_Bar" timestamp with time zone, + "MyComplex_MyNestedComplex_Foo" integer, + CONSTRAINT "PK_Contacts" PRIMARY KEY ("Id") +); +"""); + } + + 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" integer[] NOT NULL;"""); + } + + 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" integer[] NOT NULL DEFAULT ARRAY[1,2,3]::integer[];"""); + } + + [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';"); + } + + 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" text NOT NULL DEFAULT 'some numbers';"""); + } + + protected override string NonDefaultCollation => "POSIX"; diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs index dd80b867a..0a2e11cce 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs @@ -16,4 +16,9 @@ public override Task Query_generates_correct_datetime2_parameter_definition(int? public override Task Query_generates_correct_datetimeoffset_parameter_definition(int? fractionalSeconds, string postfix) => Assert.ThrowsAsync( () => base.Query_generates_correct_datetime2_parameter_definition(fractionalSeconds, postfix)); + + // Cannot write DateTimeOffset with Offset=10:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. + public override Task Projecting_one_of_two_similar_complex_types_picks_the_correct_one() + => Assert.ThrowsAsync( + () => base.Projecting_one_of_two_similar_complex_types_picks_the_correct_one()); } diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs index 6cedca28b..9ca70651d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs @@ -1023,6 +1023,29 @@ public override async Task Same_complex_type_projected_twice_with_pushdown_as_pa AssertSql(""); } + public override async Task Entity_with_complex_type_with_group_by_and_first(bool async) + { + await base.Entity_with_complex_type_with_group_by_and_first(async); + + AssertSql( + """ +SELECT c3."Id", c3."Name", c3."BillingAddress_AddressLine1", c3."BillingAddress_AddressLine2", c3."BillingAddress_Tags", c3."BillingAddress_ZipCode", c3."BillingAddress_Country_Code", c3."BillingAddress_Country_FullName", c3."ShippingAddress_AddressLine1", c3."ShippingAddress_AddressLine2", c3."ShippingAddress_Tags", c3."ShippingAddress_ZipCode", c3."ShippingAddress_Country_Code", c3."ShippingAddress_Country_FullName" +FROM ( + SELECT c."Id" + FROM "Customer" AS c + GROUP BY c."Id" +) AS c1 +LEFT JOIN ( + SELECT c2."Id", c2."Name", c2."BillingAddress_AddressLine1", c2."BillingAddress_AddressLine2", c2."BillingAddress_Tags", c2."BillingAddress_ZipCode", c2."BillingAddress_Country_Code", c2."BillingAddress_Country_FullName", c2."ShippingAddress_AddressLine1", c2."ShippingAddress_AddressLine2", c2."ShippingAddress_Tags", c2."ShippingAddress_ZipCode", c2."ShippingAddress_Country_Code", c2."ShippingAddress_Country_FullName" + FROM ( + SELECT c0."Id", c0."Name", c0."BillingAddress_AddressLine1", c0."BillingAddress_AddressLine2", c0."BillingAddress_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."ShippingAddress_AddressLine1", c0."ShippingAddress_AddressLine2", c0."ShippingAddress_Tags", c0."ShippingAddress_ZipCode", c0."ShippingAddress_Country_Code", c0."ShippingAddress_Country_FullName", ROW_NUMBER() OVER(PARTITION BY c0."Id" ORDER BY c0."Id" NULLS FIRST) AS row + FROM "Customer" AS c0 + ) AS c2 + WHERE c2.row <= 1 +) AS c3 ON c1."Id" = c3."Id" +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); 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 }; diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 134e4e820..1fab81f8c 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -340,43 +340,35 @@ await AssertQuery( t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - // TODO: https://github.com/dotnet/efcore/pull/33241 - Assert.Throws( - () => - AssertSql( - """ + AssertSql( + """ @__ToInstant_0='2018-04-20T08:31:33Z' (DbType = DateTime) SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" FROM "NodaTimeTypes" AS n WHERE n."LocalDateTime" AT TIME ZONE 'Europe/Berlin' = @__ToInstant_0 -""")); +"""); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public async Task LocalDateTime_InZoneLeniently_ToInstant_with_column_time_zone(bool async) { - // TODO: https://github.com/dotnet/efcore/pull/33241 - await Assert.ThrowsAsync( - async () => - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb[t.TimeZoneId]).ToInstant() - == new ZonedDateTime( - new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - - AssertSql( - """ + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb[t.TimeZoneId]).ToInstant() + == new ZonedDateTime( + new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ @__ToInstant_0='2018-04-20T08:31:33Z' (DbType = DateTime) SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" FROM "NodaTimeTypes" AS n WHERE n."LocalDateTime" AT TIME ZONE n."TimeZoneId" = @__ToInstant_0 """); - }); } [ConditionalFact] @@ -1501,25 +1493,20 @@ WHERE @__dateRange_0 @> n."LocalDate" [MemberData(nameof(IsAsyncData))] public async Task Instance_InUtc(bool async) { - // TODO: https://github.com/dotnet/efcore/pull/33241 - await Assert.ThrowsAsync( - async () => - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.Instant.InUtc() - == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero))); - - AssertSql( - """ + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InUtc() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero))); + + AssertSql( + """ @__p_0='2018-04-20T10:31:33 UTC (+00)' (DbType = DateTime) SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" FROM "NodaTimeTypes" AS n WHERE n."Instant" = @__p_0 """); - }); } [ConditionalTheory] @@ -1812,25 +1799,20 @@ await AssertQuery( [MemberData(nameof(IsAsyncData))] public async Task ZonedDateTime_ToInstant(bool async) { - // TODO: https://github.com/dotnet/efcore/pull/33241 - await Assert.ThrowsAsync( - async () => - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.ZonedDateTime.ToInstant() - == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - - AssertSql( - """ + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.ZonedDateTime.ToInstant() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ @__ToInstant_0='2018-04-20T10:31:33Z' (DbType = DateTime) SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" FROM "NodaTimeTypes" AS n WHERE n."ZonedDateTime" = @__ToInstant_0 """); - }); } [ConditionalFact] @@ -1838,19 +1820,15 @@ public async Task ZonedDateTime_Distance() { await using var context = CreateContext(); - // TODO: https://github.com/dotnet/efcore/pull/33241 - await Assert.ThrowsAsync( - async () => - { - var closest = await context.NodaTimeTypes - .OrderBy( - t => EF.Functions.Distance( - t.ZonedDateTime, - new ZonedDateTime(new LocalDateTime(2018, 4, 1, 0, 0, 0), DateTimeZone.Utc, Offset.Zero))).FirstAsync(); - Assert.Equal(1, closest.Id); - - AssertSql( - """ + var closest = await context.NodaTimeTypes + .OrderBy( + t => EF.Functions.Distance( + t.ZonedDateTime, + new ZonedDateTime(new LocalDateTime(2018, 4, 1, 0, 0, 0), DateTimeZone.Utc, Offset.Zero))).FirstAsync(); + Assert.Equal(1, closest.Id); + + AssertSql( + """ @__p_1='2018-04-01T00:00:00 UTC (+00)' (DbType = DateTime) SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" @@ -1858,7 +1836,6 @@ await Assert.ThrowsAsync( ORDER BY n."ZonedDateTime" <-> @__p_1 NULLS FIRST LIMIT 1 """); - }); } #endregion ZonedDateTime diff --git a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj index f828e9b41..db1b1e6d9 100644 --- a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj +++ b/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj @@ -18,6 +18,10 @@ + + + +