diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index c154a4906..672095dab 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -177,10 +177,11 @@ protected override Expression VisitNewArray(NewArrayExpression newArrayExpressio /// protected override Expression VisitBinary(BinaryExpression binaryExpression) { - if (binaryExpression.NodeType == ExpressionType.Subtract) + switch (binaryExpression.NodeType) { - if (binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate" - && binaryExpression.Right.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate") + case ExpressionType.Subtract + when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate" + && binaryExpression.Right.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate": { if (TranslationFailed(binaryExpression.Left, Visit(TryRemoveImplicitConvert(binaryExpression.Left)), out var sqlLeft) || TranslationFailed(binaryExpression.Right, Visit(TryRemoveImplicitConvert(binaryExpression.Right)), out var sqlRight)) @@ -200,20 +201,40 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) builtIn: true, _nodaTimePeriodType ??= binaryExpression.Left.Type.Assembly.GetType("NodaTime.Period")!, typeMapping: null); + + // Note: many other date/time arithmetic operators are fully supported as-is by PostgreSQL - see NpgsqlSqlExpressionFactory } - // Note: many other date/time arithmetic operators are fully supported as-is by PostgreSQL - see NpgsqlSqlExpressionFactory + case ExpressionType.ArrayIndex: + { + // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator + Check.DebugFail( + "During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator. " + + "Should never see ArrayIndex."); + break; + } } - if (binaryExpression.NodeType == ExpressionType.ArrayIndex) + var translation = base.VisitBinary(binaryExpression); + + // A somewhat hacky workaround for #2942. + // When an optional owned JSON entity is compared to null, we get WHERE (x -> y) IS NULL. + // The -> operator (returning jsonb) is used rather than ->> (returning text), since an entity type is being extracted, and further + // JSON operations may need to be composed. However, when the value extracted is a JSON null, a non-NULL jsonb value is returned, + // and comparing that to relational NULL returns false. + // Pattern-match this and force the use of ->> by changing the mapping to be a scalar rather than an entity type. + if (translation is SqlUnaryExpression + { + OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, + Operand: JsonScalarExpression { TypeMapping: NpgsqlOwnedJsonTypeMapping } operand + } unary) { - // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator - Check.DebugFail( - "During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator. " - + "Should never see ArrayIndex."); + return unary.Update( + new JsonScalarExpression( + operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); } - return base.VisitBinary(binaryExpression); + return translation; } /// diff --git a/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs index 078a7285f..3a403a72c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs @@ -85,27 +85,25 @@ public override async Task Filter_optional_dependent_with_some_required_compared public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_null(bool async) { - // #2942 - await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async)); + await base.Filter_nested_optional_dependent_with_all_optional_compared_to_null(async); AssertSql( """ SELECT e."Id", e."Name", e."Json" FROM "EntitiesAllOptional" AS e -WHERE (e."Json" -> 'OpNav2') IS NOT NULL +WHERE (e."Json" ->> 'OpNav1') IS NULL """); } public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(bool async) { - // #2942 - await Assert.ThrowsAsync(() => base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async)); + await base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async); AssertSql( """ SELECT e."Id", e."Name", e."Json" FROM "EntitiesAllOptional" AS e -WHERE (e."Json" -> 'OpNav2') IS NOT NULL +WHERE (e."Json" ->> 'OpNav2') IS NOT NULL """); }