diff --git a/Directory.Packages.props b/Directory.Packages.props index d009e9dc6..ab530711e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@ - 11.0.0-preview.1.26104.118 - 11.0.0-preview.1.26104.118 + 11.0.0-preview.2.26116.101 + 11.0.0-preview.2.26116.101 + 11.0.0-preview.2.26078.113 10.0.0 @@ -16,8 +17,8 @@ - - + + diff --git a/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs b/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs index 4ccbbe20b..e2d523c63 100644 --- a/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs +++ b/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs @@ -101,7 +101,7 @@ public static bool TryExtractJsonArray( TableValuedFunctionExpression { Name: "jsonb_array_elements_text" or "json_array_elements_text", - Arguments: [var json] + Arguments: [SqlExpression json] } tvf ], GroupBy: [], diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs index 23791956c..34f0edd0d 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs @@ -10,27 +10,15 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; /// 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 NpgsqlModelValidator : RelationalModelValidator +public class NpgsqlModelValidator( + ModelValidatorDependencies dependencies, + RelationalModelValidatorDependencies relationalDependencies, + INpgsqlSingletonOptions npgsqlSingletonOptions) : RelationalModelValidator(dependencies, relationalDependencies) { /// /// The backend version to target. /// - private readonly Version _postgresVersion; - - /// - /// 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 NpgsqlModelValidator( - ModelValidatorDependencies dependencies, - RelationalModelValidatorDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies) - { - _postgresVersion = npgsqlSingletonOptions.PostgresVersion; - } + private readonly Version _postgresVersion = npgsqlSingletonOptions.PostgresVersion; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,13 +31,10 @@ public override void Validate(IModel model, IDiagnosticsLogger - /// Validates that identity columns are used only with PostgreSQL 10.0 or later. + /// Validates that identity columns are used only with PostgreSQL 10.0 or later (model-level check). /// /// The model to validate. protected virtual void ValidateIdentityVersionCompatibility(IModel model) @@ -69,17 +54,60 @@ protected virtual void ValidateIdentityVersionCompatibility(IModel model) + $"'optionsBuilder.{nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion)}()' in your model's OnConfiguring. " + "See the docs for more info."); } + } - foreach (var property in model.GetEntityTypes().SelectMany(e => e.GetProperties())) - { - var propertyStrategy = property.GetValueGenerationStrategy(); + /// + protected override void ValidateProperty( + IProperty property, + ITypeBase structuralType, + IDiagnosticsLogger logger) + { + base.ValidateProperty(property, structuralType, logger); + + var strategy = property.GetValueGenerationStrategy(); - if (propertyStrategy is NpgsqlValueGenerationStrategy.IdentityAlwaysColumn + // Identity version compatibility (per-property check) + if (!_postgresVersion.AtLeast(10) + && strategy is NpgsqlValueGenerationStrategy.IdentityAlwaysColumn or NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - { - throw new InvalidOperationException( - $"{property.DeclaringType}.{property.Name}: '{propertyStrategy}' requires PostgreSQL 10.0 or later."); - } + { + throw new InvalidOperationException( + $"{property.DeclaringType}.{property.Name}: '{strategy}' requires PostgreSQL 10.0 or later."); + } + + // Value generation strategy compatibility + var propertyType = property.ClrType; + + switch (strategy) + { + case NpgsqlValueGenerationStrategy.None: + break; + + case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: + case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: + if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + NpgsqlStrings.IdentityBadType( + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); + } + + break; + + case NpgsqlValueGenerationStrategy.SequenceHiLo: + case NpgsqlValueGenerationStrategy.Sequence: + case NpgsqlValueGenerationStrategy.SerialColumn: + if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + NpgsqlStrings.SequenceBadType( + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); + } + + break; + + default: + throw new UnreachableException(); } } @@ -90,10 +118,10 @@ protected virtual void ValidateIdentityVersionCompatibility(IModel model) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override void ValidateValueGeneration( - IEntityType entityType, IKey key, IDiagnosticsLogger logger) { + var entityType = key.DeclaringEntityType; if (entityType.GetTableName() != null && (string?)entityType[RelationalAnnotationNames.MappingStrategy] == RelationalAnnotationNames.TpcMappingStrategy) { @@ -106,124 +134,94 @@ protected override void ValidateValueGeneration( } } - /// - protected override void ValidateTypeMappings( - IModel model, + /// + protected override void ValidateIndex( + IIndex index, IDiagnosticsLogger logger) { - base.ValidateTypeMappings(model, logger); + base.ValidateIndex(index, logger); - foreach (var entityType in model.GetEntityTypes()) + var includeProperties = index.GetIncludeProperties(); + if (includeProperties?.Count > 0) { - foreach (var property in entityType.GetFlattenedDeclaredProperties()) + var notFound = includeProperties + .FirstOrDefault(i => index.DeclaringEntityType.FindProperty(i) is null); + + if (notFound is not null) { - var strategy = property.GetValueGenerationStrategy(); - var propertyType = property.ClrType; + throw new InvalidOperationException( + NpgsqlStrings.IncludePropertyNotFound(index.DeclaringEntityType.DisplayName(), notFound)); + } - switch (strategy) - { - case NpgsqlValueGenerationStrategy.None: - break; - - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) - { - throw new InvalidOperationException( - NpgsqlStrings.IdentityBadType( - property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); - } - - break; - - case NpgsqlValueGenerationStrategy.SequenceHiLo: - case NpgsqlValueGenerationStrategy.Sequence: - case NpgsqlValueGenerationStrategy.SerialColumn: - if (!NpgsqlPropertyExtensions.IsCompatibleWithValueGeneration(property)) - { - throw new InvalidOperationException( - NpgsqlStrings.SequenceBadType( - property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); - } - - break; - - default: - throw new UnreachableException(); - } + var duplicate = includeProperties + .GroupBy(i => i) + .Where(g => g.Count() > 1) + .Select(y => y.Key) + .FirstOrDefault(); + + if (duplicate is not null) + { + throw new InvalidOperationException( + NpgsqlStrings.IncludePropertyDuplicated(index.DeclaringEntityType.DisplayName(), duplicate)); + } + + var inIndex = includeProperties + .FirstOrDefault(i => index.Properties.Any(p => i == p.Name)); + + if (inIndex is not null) + { + throw new InvalidOperationException( + NpgsqlStrings.IncludePropertyInIndex(index.DeclaringEntityType.DisplayName(), inIndex)); } } } - /// - /// 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 virtual void ValidateIndexIncludeProperties(IModel model) + /// + protected override void ValidateKey( + IKey key, + IDiagnosticsLogger logger) { - foreach (var index in model.GetEntityTypes().SelectMany(t => t.GetDeclaredIndexes())) - { - var includeProperties = index.GetIncludeProperties(); - if (includeProperties?.Count > 0) - { - var notFound = includeProperties - .FirstOrDefault(i => index.DeclaringEntityType.FindProperty(i) is null); - - if (notFound is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyNotFound(index.DeclaringEntityType.DisplayName(), notFound)); - } - - var duplicate = includeProperties - .GroupBy(i => i) - .Where(g => g.Count() > 1) - .Select(y => y.Key) - .FirstOrDefault(); + base.ValidateKey(key, logger); - if (duplicate is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyDuplicated(index.DeclaringEntityType.DisplayName(), duplicate)); - } + if (key.GetWithoutOverlaps() == true) + { + ValidateWithoutOverlapsKey(key); + } + } - var inIndex = includeProperties - .FirstOrDefault(i => index.Properties.Any(p => i == p.Name)); + /// + protected override void ValidateForeignKey( + IForeignKey foreignKey, + IDiagnosticsLogger logger) + { + base.ValidateForeignKey(foreignKey, logger); - if (inIndex is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyInIndex(index.DeclaringEntityType.DisplayName(), inIndex)); - } - } + if (foreignKey.GetPeriod() == true) + { + ValidatePeriodForeignKey(foreignKey); } } /// protected override void ValidateStoredProcedures( - IModel model, + IEntityType entityType, IDiagnosticsLogger logger) { - base.ValidateStoredProcedures(model, logger); + base.ValidateStoredProcedures(entityType, logger); - foreach (var entityType in model.GetEntityTypes()) + if (entityType.GetDeleteStoredProcedure() is { } deleteStoredProcedure) { - if (entityType.GetDeleteStoredProcedure() is { } deleteStoredProcedure) - { - ValidateSproc(deleteStoredProcedure, logger); - } + ValidateSproc(deleteStoredProcedure, logger); + } - if (entityType.GetInsertStoredProcedure() is { } insertStoredProcedure) - { - ValidateSproc(insertStoredProcedure, logger); - } + if (entityType.GetInsertStoredProcedure() is { } insertStoredProcedure) + { + ValidateSproc(insertStoredProcedure, logger); + } - if (entityType.GetUpdateStoredProcedure() is { } updateStoredProcedure) - { - ValidateSproc(updateStoredProcedure, logger); - } + if (entityType.GetUpdateStoredProcedure() is { } updateStoredProcedure) + { + ValidateSproc(updateStoredProcedure, logger); } static void ValidateSproc(IStoredProcedure sproc, IDiagnosticsLogger logger) @@ -272,27 +270,6 @@ protected override void ValidateCompatible( } } - /// - /// 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 virtual void ValidateWithoutOverlaps(IModel model) - { - foreach (var entityType in model.GetEntityTypes()) - { - // Validate primary key and alternate keys - foreach (var key in entityType.GetDeclaredKeys()) - { - if (key.GetWithoutOverlaps() == true) - { - ValidateWithoutOverlapsKey(key); - } - } - } - } - private void ValidateWithoutOverlapsKey(IKey key) { var keyName = key.IsPrimaryKey() ? "primary key" : $"alternate key {key.Properties.Format()}"; @@ -320,26 +297,6 @@ private void ValidateWithoutOverlapsKey(IKey key) } } - /// - /// 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 virtual void ValidatePeriod(IModel model) - { - foreach (var entityType in model.GetEntityTypes()) - { - foreach (var foreignKey in entityType.GetDeclaredForeignKeys()) - { - if (foreignKey.GetPeriod() == true) - { - ValidatePeriodForeignKey(foreignKey); - } - } - } - } - private void ValidatePeriodForeignKey(IForeignKey foreignKey) { var entityType = foreignKey.DeclaringEntityType; diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs index 86bcd95d9..0332f6cb0 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs @@ -21,7 +21,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// /// -public class PgTableValuedFunctionExpression : TableValuedFunctionExpression, IEquatable +public class PgTableValuedFunctionExpression( + string alias, + string name, + IReadOnlyList arguments, + IReadOnlyList? columnInfos = null, + bool withOrdinality = true) : TableValuedFunctionExpression(alias, name, schema: null, builtIn: true, arguments), IEquatable { /// /// The name of the column to be projected out from the unnest call. @@ -32,7 +37,7 @@ public class PgTableValuedFunctionExpression : TableValuedFunctionExpression, IE /// 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 virtual IReadOnlyList? ColumnInfos { get; } + public virtual IReadOnlyList? ColumnInfos { get; } = columnInfos; /// /// Whether to project an additional ordinality column containing the index of each element in the array. @@ -43,25 +48,7 @@ public class PgTableValuedFunctionExpression : TableValuedFunctionExpression, IE /// 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 virtual bool WithOrdinality { get; } - - /// - /// 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 PgTableValuedFunctionExpression( - string alias, - string name, - IReadOnlyList arguments, - IReadOnlyList? columnInfos = null, - bool withOrdinality = true) - : base(alias, name, schema: null, builtIn: true, arguments) - { - ColumnInfos = columnInfos; - WithOrdinality = withOrdinality; - } + public virtual bool WithOrdinality { get; } = withOrdinality; /// protected override Expression VisitChildren(ExpressionVisitor visitor) @@ -75,7 +62,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 override PgTableValuedFunctionExpression Update(IReadOnlyList arguments) + public override PgTableValuedFunctionExpression Update(IReadOnlyList arguments) => arguments.SequenceEqual(Arguments, ReferenceEqualityComparer.Instance) ? this : new PgTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality); @@ -83,10 +70,10 @@ public override PgTableValuedFunctionExpression Update(IReadOnlyList public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor) { - var arguments = new SqlExpression[Arguments.Count]; + var arguments = new Expression[Arguments.Count]; for (var i = 0; i < arguments.Length; i++) { - arguments[i] = (SqlExpression)cloningExpressionVisitor.Visit(Arguments[i]); + arguments[i] = cloningExpressionVisitor.Visit(Arguments[i]); } return new PgTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality); diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs index 5d1a6b31a..f80d34dbd 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs @@ -32,8 +32,7 @@ public class PgUnnestExpression : PgTableValuedFunctionExpression /// 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 virtual SqlExpression Array - => Arguments[0]; + public virtual SqlExpression Array { get; } /// /// The name of the column to be projected out from the unnest call. @@ -60,8 +59,7 @@ public PgUnnestExpression(string alias, SqlExpression array, string columnName, private PgUnnestExpression(string alias, SqlExpression array, ColumnInfo? columnInfo, bool withOrdinality = true) : base(alias, "unnest", [array], columnInfo is null ? null : [columnInfo.Value], withOrdinality) - { - } + => Array = array; /// protected override Expression VisitChildren(ExpressionVisitor visitor) @@ -75,8 +73,8 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 override PgUnnestExpression Update(IReadOnlyList arguments) - => arguments is [var singleArgument] + public override PgUnnestExpression Update(IReadOnlyList arguments) + => arguments is [SqlExpression singleArgument] ? Update(singleArgument) : throw new ArgumentException(); diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs index eeb6413d5..4d6c19f37 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs @@ -9,7 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// See for the older Npgsql-specific support, which allows mapping json/jsonb to text, to e.g. /// (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled). /// -public class NpgsqlStructuralJsonTypeMapping : JsonTypeMapping +public class NpgsqlStructuralJsonTypeMapping : StructuralJsonTypeMapping { /// /// The database type used by Npgsql ( or . diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs index 66e8b515b..34bbda80b 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs @@ -5,7 +5,8 @@ public class NpgsqlComplianceTest : RelationalComplianceTestBase protected override ICollection IgnoredTestBases { get; } = new HashSet { // Not implemented - typeof(CompiledModelTestBase), typeof(CompiledModelRelationalTestBase), // #3087 + typeof(CompiledModelTestBase), + typeof(CompiledModelRelationalTestBase), // #3087 typeof(FromSqlSprocQueryTestBase<>), typeof(UdfDbFunctionTestBase<>), typeof(UpdateSqlGeneratorTestBase), diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs index 1004c7a98..866f8516f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs @@ -151,7 +151,7 @@ public override async Task Select_unmapped_associate_scalar_property(QueryTracki AssertSql( """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" +SELECT r."RequiredAssociate" FROM "RootEntity" AS r """); } @@ -279,6 +279,20 @@ public override async Task Select_root_duplicated(QueryTrackingBehavior queryTra """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate" +FROM "RootEntity" AS r +"""); + } + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs index 87a40359b..85c01e04f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs @@ -153,7 +153,7 @@ public override async Task Select_unmapped_associate_scalar_property(QueryTracki AssertSql( """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +SELECT r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" FROM "RootEntity" AS r """); } @@ -191,7 +191,7 @@ public override async Task Select_nested_collection_on_required_associate(QueryT AssertSql( """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +SELECT r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" FROM "RootEntity" AS r ORDER BY r."Id" NULLS FIRST """); @@ -203,7 +203,7 @@ public override async Task Select_nested_collection_on_optional_associate(QueryT AssertSql( """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +SELECT r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" FROM "RootEntity" AS r ORDER BY r."Id" NULLS FIRST """); @@ -248,6 +248,17 @@ public override async Task Select_root_duplicated(QueryTrackingBehavior queryTra """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS r +"""); + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs index c3252acd0..7ca2d8177 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs @@ -349,6 +349,22 @@ LEFT JOIN ( """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + AssertSql( + """ +SELECT r."Id", a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" +FROM "RootEntity" AS r +INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" +LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" +INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" +LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" +ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST +"""); + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs index 26200c5a4..70b5972de 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs @@ -321,6 +321,20 @@ public override async Task Select_root_duplicated(QueryTrackingBehavior queryTra """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate" +FROM "RootEntity" AS r +"""); + } + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs index 24c01fda7..f1ac0c908 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs @@ -391,6 +391,25 @@ LEFT JOIN ( """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" +LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" +LEFT JOIN "RequiredRelated_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs index 4000b9bb6..ae8316473 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs @@ -352,6 +352,22 @@ WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT N """); } + public override async Task Select_associate_and_target_to_index_based_binding_via_closure(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_and_target_to_index_based_binding_via_closure(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" +FROM "RootEntity" AS r +LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST +"""); + } + } + #endregion Multiple #region Subquery diff --git a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs index 1eaa9ea66..d990d5d02 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs @@ -34,7 +34,7 @@ public override async Task FromSqlInterpolated_queryable_multiple_composed_with_ var query = from c in context.Set().FromSqlRaw( NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( + from o in context.Set().FromSql( NormalizeDelimitersInInterpolatedString( $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) where c.CustomerID == o.CustomerID @@ -53,7 +53,7 @@ from o in context.Set().FromSqlInterpolated( query = (from c in context.Set().FromSqlRaw( NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( + from o in context.Set().FromSql( NormalizeDelimitersInInterpolatedString( $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) where c.CustomerID == o.CustomerID @@ -80,7 +80,7 @@ public override async Task FromSql_queryable_multiple_composed_with_parameters_a var query = from c in context.Set().FromSqlRaw( NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( + from o in context.Set().FromSql( NormalizeDelimitersInInterpolatedString( $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) where c.CustomerID == o.CustomerID @@ -99,7 +99,7 @@ from o in context.Set().FromSqlInterpolated( query = (from c in context.Set().FromSqlRaw( NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( + from o in context.Set().FromSql( NormalizeDelimitersInInterpolatedString( $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) where c.CustomerID == o.CustomerID diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs index cfb8de908..0af9403ea 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs @@ -505,7 +505,7 @@ public void JsonExists() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonExists(e.CustomerElement.GetProperty("Statistics"), "Visits")); + NpgsqlJsonDbFunctionsExtensions.JsonExists(EF.Functions, e.CustomerElement.GetProperty("Statistics"), "Visits")); Assert.Equal(2, count); AssertSql( diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs index ffc1ae259..7a552a736 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs @@ -614,7 +614,7 @@ public void JsonExists() using var ctx = CreateContext(); var count = ctx.JsonbEntities.Count( e => - EF.Functions.JsonExists(e.Customer.Statistics, "Visits")); + NpgsqlJsonDbFunctionsExtensions.JsonExists(EF.Functions, e.Customer.Statistics, "Visits")); Assert.Equal(2, count); AssertSql( diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs index b8b890920..ae4305cee 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs @@ -194,7 +194,7 @@ public void JsonExists() { using var ctx = CreateContext(); - var count = ctx.JsonEntities.Count(e => EF.Functions.JsonExists(e.CustomerJsonb, "Age")); + var count = ctx.JsonEntities.Count(e => NpgsqlJsonDbFunctionsExtensions.JsonExists(EF.Functions, e.CustomerJsonb, "Age")); Assert.Equal(2, count); diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs index 5ef3dd624..9bfd4ff62 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs @@ -104,6 +104,29 @@ GROUP BY o."CustomerID" """); } + public override async Task GroupBy_Property_Select_MaxBy(bool async) + { + await base.GroupBy_Property_Select_MaxBy(async); + + AssertSql( + """ +SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o1 +LEFT JOIN ( + SELECT o2."OrderID", o2."CustomerID", o2."EmployeeID", o2."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" DESC NULLS LAST) AS row + FROM "Orders" AS o0 + ) AS o2 + WHERE o2.row <= 1 +) AS o3 ON o1."CustomerID" = o3."CustomerID" +"""); + } + public override async Task GroupBy_Property_Select_Min(bool async) { await base.GroupBy_Property_Select_Min(async); @@ -116,6 +139,29 @@ GROUP BY o."CustomerID" """); } + public override async Task GroupBy_Property_Select_MinBy(bool async) + { + await base.GroupBy_Property_Select_MinBy(async); + + AssertSql( + """ +SELECT o3."OrderID", o3."CustomerID", o3."EmployeeID", o3."OrderDate" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o1 +LEFT JOIN ( + SELECT o2."OrderID", o2."CustomerID", o2."EmployeeID", o2."OrderDate" + FROM ( + SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", ROW_NUMBER() OVER(PARTITION BY o0."CustomerID" ORDER BY o0."OrderID" NULLS FIRST) AS row + FROM "Orders" AS o0 + ) AS o2 + WHERE o2.row <= 1 +) AS o3 ON o1."CustomerID" = o3."CustomerID" +"""); + } + public override async Task GroupBy_Property_Select_Sum(bool async) { await base.GroupBy_Property_Select_Sum(async); diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index 7b7ade293..11c5da52f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -654,6 +654,21 @@ WHERE NOT (p."NullableInt" = ANY (@nullableInts) AND p."NullableInt" = ANY (@nul """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts={ NULL +'999' } (DbType = Object) + +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" = ANY (@nullableInts) OR (p."NullableInt" IS NULL AND array_position(@nullableInts, NULL) IS NOT NULL) +"""); + } + public override async Task Parameter_collection_of_structs_Contains_struct() { await base.Parameter_collection_of_structs_Contains_struct(); @@ -1103,9 +1118,21 @@ WHERE array_position(p."NullableInts", NULL) IS NOT NULL """); } - public override async Task Column_collection_of_strings_contains_null() + public override async Task Column_collection_of_strings_Contains() + { + await base.Column_collection_of_strings_Contains(); + + AssertSql( + """ +SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE '10' = ANY (p."Strings") +"""); + } + + public override async Task Column_collection_of_strings_Contains_null() { - await base.Column_collection_of_strings_contains_null(); + await base.Column_collection_of_strings_Contains_null(); AssertSql( """ diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/JsonTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/JsonTranslationsNpgsqlTest.cs new file mode 100644 index 000000000..9e0586970 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/JsonTranslationsNpgsqlTest.cs @@ -0,0 +1,72 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class JsonTranslationsNpgsqlTest : JsonTranslationsRelationalTestBase +{ + public JsonTranslationsNpgsqlTest(JsonTranslationsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + + public override async Task JsonExists_on_scalar_string_column() + { + // TODO: #3733 + await AssertTranslationFailed(base.JsonExists_on_scalar_string_column); + + AssertSql(); + } + + public override async Task JsonExists_on_complex_property() + { + // TODO: #3733 + await AssertTranslationFailed(base.JsonExists_on_complex_property); + + AssertSql(); + } + + public override async Task JsonExists_on_owned_entity() + { + // TODO: #3733 + await AssertTranslationFailed(base.JsonExists_on_owned_entity); + + AssertSql(); + } + + public class JsonTranslationsQueryNpgsqlFixture : JsonTranslationsQueryFixtureBase, ITestSqlLoggerFactory + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().Property(e => e.JsonString).HasColumnType("jsonb"); + } + + protected override string RemoveJsonProperty(string column, string jsonPath) + { + // HACK. PostgreSQL doesn't have a delete function accepting JSON path, but the base class requires this + // only for a single path segment, which we can do. Rethink this mechanism in EF. + if (jsonPath.StartsWith("$.")) + { + var segment = jsonPath[2..]; + if (!segment.Contains('.')) + { + return $"{column} - '{segment}'"; + } + } + + throw new UnreachableException(); + } + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}