diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs index fae6ab697f..6c5db36bfb 100644 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs +++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs @@ -55,6 +55,24 @@ public static T[] JsonbAgg(this DbFunctions _, IEnumerable input) public static TimeSpan? Average(this DbFunctions _, IEnumerable input) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); + /// + /// Returns true if all non-null input values are true, otherwise false. Corresponds to the PostgreSQL bool_and aggregate function. + /// + /// The instance. + /// The input values to be AND'ed together. + /// PostgreSQL documentation for aggregate functions. + public static bool BoolAnd(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BoolAnd))); + + /// + /// Returns true if any non-null input value is true, otherwise false. Corresponds to the PostgreSQL bool_or aggregate function. + /// + /// The instance. + /// The input values to be OR'ed together. + /// PostgreSQL documentation for aggregate functions. + public static bool BoolOr(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BoolOr))); + // See additional range aggregate functions in NpgsqlRangeDbfunctionsExtensions #region JsonObjectAgg diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs index b938ecb96d..d199f73fcc 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs @@ -163,6 +163,26 @@ public NpgsqlMiscAggregateMethodTranslator( argumentsPropagateNullability: FalseArrays[2], returnType: method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, isJsonb ? "jsonb" : "json")); + + case nameof(NpgsqlAggregateDbFunctionsExtensions.BoolAnd): + return _sqlExpressionFactory.AggregateFunction( + "bool_and", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, + sqlExpression.TypeMapping); + + case nameof(NpgsqlAggregateDbFunctionsExtensions.BoolOr): + return _sqlExpressionFactory.AggregateFunction( + "bool_or", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, + sqlExpression.TypeMapping); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs index 1cffc45fe8..02552114d8 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs @@ -178,6 +178,31 @@ public NpgsqlQueryableAggregateMethodTranslator( argumentsPropagateNullability: FalseArrays[1], sumInputType, sumSqlExpression.TypeMapping); + + case nameof(Queryable.Any) + when (methodInfo == QueryableMethods.AnyWithoutPredicate + || methodInfo == QueryableMethods.AnyWithPredicate) + && source.Selector is SqlExpression anySqlExpression: + return _sqlExpressionFactory.AggregateFunction( + "bool_any", + [anySqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + anySqlExpression.Type, + anySqlExpression.TypeMapping); + + case nameof(Queryable.All) + when (methodInfo == QueryableMethods.All) + && source.Selector is SqlExpression allSqlExpression: + return _sqlExpressionFactory.AggregateFunction( + "bool_all", + [allSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + allSqlExpression.Type, + allSqlExpression.TypeMapping); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs index 18bfe6e090..b6045a434d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs @@ -231,6 +231,48 @@ GROUP BY m."Id" """); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_Property_Select_BoolAnd_over_Bool(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.CodeName) + .Select(g => EF.Functions.BoolAnd(g.Select(o => o.Id == 1))), + ss => ss.Set() + .GroupBy(o => o.CodeName) + .Select(g => g.All(o => o.Id == 1))); + + AssertSql( + """ +SELECT bool_and(m."Id" = 1) +FROM "Missions" AS m +GROUP BY m."CodeName" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_Property_Select_BoolOr_over_Bool(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.CodeName) + .Select(g => EF.Functions.BoolOr(g.Select(o => o.Id == 1))), + ss => ss.Set() + .GroupBy(o => o.CodeName) + .Select(g => g.Any(o => o.Id == 1))); + + AssertSql( + """ +SELECT bool_or(m."Id" = 1) +FROM "Missions" AS m +GROUP BY m."CodeName" +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }