未验证 提交 82a758cd 编写于 作者: S Shay Rojansky 提交者: GitHub

Translate PostgreSQL custom aggregates (#2383)

* string_agg (string.Join)
* array_agg
* json_agg/jsonb_agg
* json_object_agg/jsonb_object_agg
* range_agg, range_intersect_agg
* Aggregate statistics functions

Closes #2395
Closes #532
上级 037cffd0
......@@ -64,9 +64,9 @@ public class NpgsqlNetTopologySuiteAggregateMethodTranslator : IAggregateMethodC
return _sqlExpressionFactory.AggregateFunction(
method == GeometryCombineMethod ? "ST_Collect" : "ST_Union",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: new[] { false },
source,
typeof(Geometry),
resultTypeMapping);
}
......@@ -80,9 +80,9 @@ public class NpgsqlNetTopologySuiteAggregateMethodTranslator : IAggregateMethodC
_sqlExpressionFactory.AggregateFunction(
"ST_Collect",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: new[] { false },
source,
typeof(Geometry),
resultTypeMapping)
},
......
......@@ -11,7 +11,9 @@ public NpgsqlAggregateMethodCallTranslatorProvider(RelationalAggregateMethodCall
AddTranslators(
new IAggregateMethodCallTranslator[]
{
new NpgsqlQueryableAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource)
new NpgsqlQueryableAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource),
new NpgsqlStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource),
new NpgsqlMiscAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource)
});
}
}
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
public class NpgsqlMiscAggregateMethodTranslator : IAggregateMethodCallTranslator
{
private static readonly MethodInfo StringJoin
= typeof(string).GetRuntimeMethod(nameof(string.Join), new[] { typeof(string), typeof(IEnumerable<string>) })!;
private static readonly MethodInfo StringConcat
= typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(IEnumerable<string>) })!;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;
public NpgsqlMiscAggregateMethodTranslator(
NpgsqlSqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
{
_sqlExpressionFactory = sqlExpressionFactory;
_typeMappingSource = typeMappingSource;
}
public virtual SqlExpression? Translate(
MethodInfo method,
EnumerableExpression source,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
// Docs: https://www.postgresql.org/docs/current/functions-aggregate.html
if (source.Selector is not SqlExpression sqlExpression)
{
return null;
}
if (method == StringJoin || method == StringConcat)
{
// string_agg filters out nulls, but string.Join treats them as empty strings; coalesce unless we know we're aggregating over
// a non-nullable column.
if (sqlExpression is not ColumnExpression { IsNullable: false })
{
sqlExpression = _sqlExpressionFactory.Coalesce(
sqlExpression,
_sqlExpressionFactory.Constant(string.Empty, typeof(string)));
}
// string_agg returns null when there are no rows (or non-null values), but string.Join returns an empty string.
return _sqlExpressionFactory.Coalesce(
_sqlExpressionFactory.AggregateFunction(
"string_agg",
new[]
{
sqlExpression,
method == StringJoin ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string))
},
source,
nullable: true,
argumentsPropagateNullability: new[] { false, true },
typeof(string),
_typeMappingSource.FindMapping("text")), // Note that string_agg returns text even if its inputs are varchar(x)
_sqlExpressionFactory.Constant(string.Empty, typeof(string)));
}
if (method.DeclaringType == typeof(NpgsqlAggregateDbFunctionsExtensions))
{
switch (method.Name)
{
case nameof(NpgsqlAggregateDbFunctionsExtensions.ArrayAgg):
var arrayClrType = sqlExpression.Type.MakeArrayType();
return _sqlExpressionFactory.AggregateFunction(
"array_agg",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: arrayClrType,
typeMapping: sqlExpression.TypeMapping is null
? null
: new NpgsqlArrayArrayTypeMapping(arrayClrType, sqlExpression.TypeMapping));
case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonAgg):
arrayClrType = sqlExpression.Type.MakeArrayType();
return _sqlExpressionFactory.AggregateFunction(
"json_agg",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: arrayClrType,
_typeMappingSource.FindMapping(arrayClrType, "json"));
case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbAgg):
arrayClrType = sqlExpression.Type.MakeArrayType();
return _sqlExpressionFactory.AggregateFunction(
"jsonb_agg",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: arrayClrType,
_typeMappingSource.FindMapping(arrayClrType, "jsonb"));
case nameof(NpgsqlAggregateDbFunctionsExtensions.RangeAgg):
arrayClrType = sqlExpression.Type.MakeArrayType();
return _sqlExpressionFactory.AggregateFunction(
"range_agg",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: arrayClrType,
_typeMappingSource.FindMapping(arrayClrType));
case nameof(NpgsqlAggregateDbFunctionsExtensions.RangeIntersectAgg):
return _sqlExpressionFactory.AggregateFunction(
"range_intersect_agg",
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: sqlExpression.Type,
sqlExpression.TypeMapping);
case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbObjectAgg):
case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonObjectAgg):
var isJsonb = method.Name == nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbObjectAgg);
// These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping
// to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart
// here.
if (source.Selector is not PostgresRowValueExpression rowValueExpression)
{
return null;
}
var (keys, values) = (rowValueExpression.Values[0], rowValueExpression.Values[1]);
return _sqlExpressionFactory.AggregateFunction(
isJsonb ? "jsonb_object_agg" : "json_object_agg",
new[] { keys, values },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[2],
returnType: method.ReturnType,
_typeMappingSource.FindMapping(method.ReturnType, isJsonb ? "jsonb" : "json"));
}
}
return null;
}
}
......@@ -45,18 +45,18 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
_sqlExpressionFactory.AggregateFunction(
"avg",
new[] { averageSqlExpression },
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
typeof(double)),
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
returnType: typeof(double)),
averageSqlExpression.Type,
averageSqlExpression.TypeMapping)
: _sqlExpressionFactory.AggregateFunction(
"avg",
new[] { averageSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
averageSqlExpression.Type,
averageSqlExpression.TypeMapping);
......@@ -69,11 +69,12 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
_sqlExpressionFactory.AggregateFunction(
"count",
new[] { countSqlExpression },
source,
nullable: false,
argumentsPropagateNullability: FalseArrays[1],
source,
typeof(long)),
typeof(int), _typeMappingSource.FindMapping(typeof(int)));
typeof(int),
_typeMappingSource.FindMapping(typeof(int)));
case nameof(Queryable.LongCount)
when methodInfo == QueryableMethods.LongCountWithoutPredicate
......@@ -82,9 +83,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
return _sqlExpressionFactory.AggregateFunction(
"count",
new[] { longCountSqlExpression },
source,
nullable: false,
argumentsPropagateNullability: FalseArrays[1],
source,
typeof(long));
case nameof(Queryable.Max)
......@@ -94,9 +95,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
return _sqlExpressionFactory.AggregateFunction(
"max",
new[] { maxSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
maxSqlExpression.Type,
maxSqlExpression.TypeMapping);
......@@ -107,9 +108,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
return _sqlExpressionFactory.AggregateFunction(
"min",
new[] { minSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
minSqlExpression.Type,
minSqlExpression.TypeMapping);
......@@ -129,9 +130,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
_sqlExpressionFactory.AggregateFunction(
"sum",
new[] { sumSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
typeof(long)),
sumInputType,
sumSqlExpression.TypeMapping);
......@@ -143,9 +144,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
_sqlExpressionFactory.AggregateFunction(
"sum",
new[] { sumSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
typeof(decimal)),
sumInputType,
sumSqlExpression.TypeMapping);
......@@ -154,9 +155,9 @@ public class NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTran
return _sqlExpressionFactory.AggregateFunction(
"sum",
new[] { sumSqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
source,
sumInputType,
sumSqlExpression.TypeMapping);
}
......
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
public class NpgsqlStatisticsAggregateMethodTranslator : IAggregateMethodCallTranslator
{
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly RelationalTypeMapping _doubleTypeMapping, _longTypeMapping;
public NpgsqlStatisticsAggregateMethodTranslator(
NpgsqlSqlExpressionFactory sqlExpressionFactory,
IRelationalTypeMappingSource typeMappingSource)
{
_sqlExpressionFactory = sqlExpressionFactory;
_doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!;
_longTypeMapping = typeMappingSource.FindMapping(typeof(long))!;
}
public virtual SqlExpression? Translate(
MethodInfo method,
EnumerableExpression source,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
// Docs: https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-STATISTICS-TABLE
if (method.DeclaringType != typeof(NpgsqlAggregateDbFunctionsExtensions)
|| source.Selector is not SqlExpression sqlExpression)
{
return null;
}
// These four functions are simple and take a single enumerable argument
var functionName = method.Name switch
{
nameof(NpgsqlAggregateDbFunctionsExtensions.StandardDeviationSample) => "stddev_samp",
nameof(NpgsqlAggregateDbFunctionsExtensions.StandardDeviationPopulation) => "stddev_pop",
nameof(NpgsqlAggregateDbFunctionsExtensions.VarianceSample) => "var_samp",
nameof(NpgsqlAggregateDbFunctionsExtensions.VariancePopulation) => "var_pop",
_ => null
};
if (functionName is not null)
{
return _sqlExpressionFactory.AggregateFunction(
functionName,
new[] { sqlExpression },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[1],
typeof(double),
_doubleTypeMapping);
}
functionName = method.Name switch
{
nameof(NpgsqlAggregateDbFunctionsExtensions.Correlation) => "corr",
nameof(NpgsqlAggregateDbFunctionsExtensions.CovariancePopulation) => "covar_pop",
nameof(NpgsqlAggregateDbFunctionsExtensions.CovarianceSample) => "covar_samp",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrAverageX) => "regr_avgx",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrAverageY) => "regr_avgy",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrCount) => "regr_count",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrIntercept) => "regr_intercept",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrR2) => "regr_r2",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSlope) => "regr_slope",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSXX) => "regr_sxx",
nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSXY) => "regr_sxy",
_ => null
};
if (functionName is not null)
{
// These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping
// to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart here.
if (source.Selector is not PostgresRowValueExpression rowValueExpression)
{
return null;
}
var (y, x) = (rowValueExpression.Values[0], rowValueExpression.Values[1]);
return method.Name == nameof(NpgsqlAggregateDbFunctionsExtensions.RegrCount)
? _sqlExpressionFactory.AggregateFunction(
functionName,
new[] { y, x },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[2],
typeof(long),
_longTypeMapping)
: _sqlExpressionFactory.AggregateFunction(
functionName,
new[] { y, x },
source,
nullable: true,
argumentsPropagateNullability: FalseArrays[2],
typeof(double),
_doubleTypeMapping);
}
return null;
}
}
......@@ -58,9 +58,8 @@ public new virtual IReadOnlyList<bool> ArgumentsPropagateNullability
return new PostgresFunctionExpression(
name, arguments, argumentNames, argumentSeparators: null,
nullable, argumentsPropagateNullability,
aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: Array.Empty<OrderingExpression>(),
type, typeMapping);
nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping);
}
public static PostgresFunctionExpression CreateWithArgumentSeparators(
......@@ -77,10 +76,9 @@ public new virtual IReadOnlyList<bool> ArgumentsPropagateNullability
Check.NotNull(argumentSeparators, nameof(argumentSeparators));
return new PostgresFunctionExpression(
name, arguments, argumentNames: null, argumentSeparators,
nullable, argumentsPropagateNullability,
name, arguments, argumentNames: null, argumentSeparators: argumentSeparators,
aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: Array.Empty<OrderingExpression>(),
type, typeMapping);
nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping);
}
public PostgresFunctionExpression(
......@@ -88,11 +86,11 @@ public new virtual IReadOnlyList<bool> ArgumentsPropagateNullability
IEnumerable<SqlExpression> arguments,
IEnumerable<string?>? argumentNames,
IEnumerable<string?>? argumentSeparators,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
bool aggregateDistinct,
SqlExpression? aggregatePredicate,
IReadOnlyList<OrderingExpression> aggregateOrderings,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
Type type,
RelationalTypeMapping? typeMapping)
: base(name, arguments, nullable, argumentsPropagateNullability, type, typeMapping)
......@@ -175,11 +173,10 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
return changed
? new PostgresFunctionExpression(
Name, visitedArguments ?? Arguments, ArgumentNames, ArgumentSeparators,
IsNullable, ArgumentsPropagateNullability!,
IsAggregateDistinct,
visitedAggregatePredicate ?? AggregatePredicate,
visitedAggregateOrderings ?? AggregateOrderings,
Type, TypeMapping)
IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping)
: this;
}
......@@ -189,13 +186,11 @@ public override SqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? ty
Arguments,
ArgumentNames,
ArgumentSeparators,
IsNullable,
ArgumentsPropagateNullability,
IsAggregateDistinct,
AggregatePredicate,
AggregateOrderings,
Type,
typeMapping ?? TypeMapping);
IsNullable,
ArgumentsPropagateNullability, Type, typeMapping ?? TypeMapping);
public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList<SqlExpression>? arguments)
{
......@@ -209,11 +204,10 @@ public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyL
return !arguments.SequenceEqual(Arguments)
? new PostgresFunctionExpression(
Name, arguments, ArgumentNames, ArgumentSeparators,
IsNullable, ArgumentsPropagateNullability,
IsAggregateDistinct,
AggregatePredicate,
AggregateOrderings,
Type, TypeMapping)
IsNullable, ArgumentsPropagateNullability, Type, TypeMapping)
: this;
}
......@@ -224,11 +218,10 @@ public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyL
return predicate != AggregatePredicate || orderings != AggregateOrderings
? new PostgresFunctionExpression(
Name, Arguments, ArgumentNames, ArgumentSeparators,
IsNullable, ArgumentsPropagateNullability,
IsAggregateDistinct,
predicate,
orderings,
Type, TypeMapping)
IsNullable, ArgumentsPropagateNullability, Type, TypeMapping)
: this;
}
......
......@@ -283,9 +283,9 @@ public virtual PostgresBinaryExpression Overlaps(SqlExpression left, SqlExpressi
public virtual PostgresFunctionExpression AggregateFunction(
string name,
IEnumerable<SqlExpression> arguments,
EnumerableExpression aggregateEnumerableExpression,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
EnumerableExpression aggregateEnumerableExpression,
Type returnType,
RelationalTypeMapping? typeMapping = null)
{
......@@ -301,13 +301,11 @@ public virtual PostgresBinaryExpression Overlaps(SqlExpression left, SqlExpressi
typeMappedArguments,
argumentNames: null,
argumentSeparators: null,
nullable,
argumentsPropagateNullability,
aggregateEnumerableExpression.IsDistinct,
aggregateEnumerableExpression.Predicate,
aggregateEnumerableExpression.Orderings,
returnType,
typeMapping);
nullable: nullable,
argumentsPropagateNullability: argumentsPropagateNullability, type: returnType, typeMapping: typeMapping);
}
#endregion Expression factory methods
......
......@@ -438,6 +438,34 @@ public void Intersect_multirange()
LIMIT 2");
}
[ConditionalFact]
public void Intersect_aggregate()
{
using var context = CreateContext();
var intersection = context.TestEntities
.Where(x => x.Id == 1 || x.Id == 2)
.GroupBy(x => true)
.Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.IntMultirange)))
.Single();
Assert.Equal(new NpgsqlRange<int>[]
{
new(4, true, 6, false),
new(7, true, 9, false)
}, intersection);
AssertSql(
@"SELECT range_intersect_agg(t0.""IntMultirange"")
FROM (
SELECT t.""IntMultirange"", TRUE AS ""Key""
FROM ""TestEntities"" AS t
WHERE t.""Id"" IN (1, 2)
) AS t0
GROUP BY t0.""Key""
LIMIT 2");
}
[ConditionalFact]
public void Except_multirange()
{
......
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
......@@ -12,7 +13,7 @@ public class NorthwindFunctionsQueryNpgsqlTest : NorthwindFunctionsQueryRelation
: base(fixture)
{
ClearLog();
//Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
public override async Task IsNullOrWhiteSpace_in_predicate(bool async)
......@@ -275,6 +276,431 @@ public Task PadRight_char_with_parameter(bool async)
#endregion
#region Aggregate functions
public override async Task String_Join_over_non_nullable_column(bool async)
{
await base.String_Join_over_non_nullable_column(async);
AssertSql(
@"SELECT c.""City"", COALESCE(string_agg(c.""CustomerID"", '|'), '') AS ""Customers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
public override async Task String_Join_over_nullable_column(bool async)
{
await base.String_Join_over_nullable_column(async);
AssertSql(
@"SELECT c.""City"", COALESCE(string_agg(COALESCE(c.""Region"", ''), '|'), '') AS ""Regions""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
public override async Task String_Join_with_predicate(bool async)
{
await base.String_Join_with_predicate(async);
AssertSql(
@"SELECT c.""City"", COALESCE(string_agg(c.""CustomerID"", '|') FILTER (WHERE length(c.""ContactName"")::int > 10), '') AS ""Customers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
public override async Task String_Join_with_ordering(bool async)
{
await base.String_Join_with_ordering(async);
AssertSql(
@"SELECT c.""City"", COALESCE(string_agg(c.""CustomerID"", '|' ORDER BY c.""CustomerID"" DESC NULLS LAST), '') AS ""Customers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
public override async Task String_Concat(bool async)
{
await base.String_Concat(async);
AssertSql(
@"SELECT c.""City"", COALESCE(string_agg(c.""CustomerID"", ''), '') AS ""Customers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_ArrayAgg(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(g => new
{
City = g.Key,
FaxNumbers = EF.Functions.ArrayAgg(g.Select(c => c.Fax).OrderBy(id => id))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(x => x.City == "London");
Assert.Collection(
london.FaxNumbers,
Assert.Null,
f => Assert.Equal("(171) 555-2530", f),
f => Assert.Equal("(171) 555-3373", f),
f => Assert.Equal("(171) 555-5646", f),
f => Assert.Equal("(171) 555-6750", f),
f => Assert.Equal("(171) 555-9199", f));
AssertSql(
@"SELECT c.""City"", array_agg(c.""Fax"" ORDER BY c.""Fax"" NULLS FIRST) AS ""FaxNumbers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonAgg(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(g => new
{
City = g.Key,
FaxNumbers = EF.Functions.JsonAgg(g.Select(c => c.Fax).OrderBy(id => id))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(x => x.City == "London");
Assert.Collection(
london.FaxNumbers,
Assert.Null,
f => Assert.Equal("(171) 555-2530", f),
f => Assert.Equal("(171) 555-3373", f),
f => Assert.Equal("(171) 555-5646", f),
f => Assert.Equal("(171) 555-6750", f),
f => Assert.Equal("(171) 555-9199", f));
AssertSql(
@"SELECT c.""City"", json_agg(c.""Fax"" ORDER BY c.""Fax"" NULLS FIRST) AS ""FaxNumbers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonbAgg(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(g => new
{
City = g.Key,
FaxNumbers = EF.Functions.JsonbAgg(g.Select(c => c.Fax).OrderBy(id => id))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(x => x.City == "London");
Assert.Collection(
london.FaxNumbers,
Assert.Null,
f => Assert.Equal("(171) 555-2530", f),
f => Assert.Equal("(171) 555-3373", f),
f => Assert.Equal("(171) 555-5646", f),
f => Assert.Equal("(171) 555-6750", f),
f => Assert.Equal("(171) 555-9199", f));
AssertSql(
@"SELECT c.""City"", jsonb_agg(c.""Fax"" ORDER BY c.""Fax"" NULLS FIRST) AS ""FaxNumbers""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
#endregion Aggregate functions
#region JsonObjectAgg
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonObjectAgg(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(
g => new
{
City = g.Key,
Companies = EF.Functions.JsonObjectAgg(
g
.OrderBy(c => c.CompanyName)
.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName)))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(r => r.City == "London");
Assert.Equal(
@"{ ""Around the Horn"" : ""Thomas Hardy"", ""B's Beverages"" : ""Victoria Ashworth"", ""Consolidated Holdings"" : ""Elizabeth Brown"", ""Eastern Connection"" : ""Ann Devon"", ""North/South"" : ""Simon Crowther"", ""Seven Seas Imports"" : ""Hari Kumar"" }",
london.Companies);
AssertSql(
@"SELECT c.""City"", json_object_agg(c.""CompanyName"", c.""ContactName"" ORDER BY c.""CompanyName"" NULLS FIRST) AS ""Companies""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonObjectAgg_as_Dictionary(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(
g => new
{
City = g.Key,
Companies = EF.Functions.JsonObjectAgg<string, string, Dictionary<string, string>>(
g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName)))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(r => r.City == "London");
Assert.Equal(
new Dictionary<string, string>
{
["Around the Horn"] = "Thomas Hardy",
["B's Beverages"] = "Victoria Ashworth",
["Consolidated Holdings"] = "Elizabeth Brown",
["Eastern Connection"] = "Ann Devon",
["North/South"] = "Simon Crowther",
["Seven Seas Imports"] = "Hari Kumar"
},
london.Companies);
AssertSql(
@"SELECT c.""City"", json_object_agg(c.""CompanyName"", c.""ContactName"") AS ""Companies""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonbObjectAgg(bool async)
{
using var ctx = CreateContext();
// Note that unlike with json, jsonb doesn't guarantee ordering; so we parse the JSON string client-side.
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(
g => new
{
City = g.Key,
Companies = EF.Functions.JsonbObjectAgg(g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName)))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(r => r.City == "London");
var companiesDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(london.Companies);
Assert.Equal(
new Dictionary<string, string>
{
["Around the Horn"] = "Thomas Hardy",
["B's Beverages"] = "Victoria Ashworth",
["Consolidated Holdings"] = "Elizabeth Brown",
["Eastern Connection"] = "Ann Devon",
["North/South"] = "Simon Crowther",
["Seven Seas Imports"] = "Hari Kumar"
},
companiesDictionary);
AssertSql(
@"SELECT c.""City"", jsonb_object_agg(c.""CompanyName"", c.""ContactName"") AS ""Companies""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_JsonbObjectAgg_as_Dictionary(bool async)
{
using var ctx = CreateContext();
var query = ctx.Set<Customer>()
.GroupBy(c => c.City)
.Select(
g => new
{
City = g.Key,
Companies = EF.Functions.JsonbObjectAgg<string, string, Dictionary<string, string>>(
g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName)))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var london = results.Single(r => r.City == "London");
Assert.Equal(
new Dictionary<string, string>
{
["Around the Horn"] = "Thomas Hardy",
["B's Beverages"] = "Victoria Ashworth",
["Consolidated Holdings"] = "Elizabeth Brown",
["Eastern Connection"] = "Ann Devon",
["North/South"] = "Simon Crowther",
["Seven Seas Imports"] = "Hari Kumar"
},
london.Companies);
AssertSql(
@"SELECT c.""City"", jsonb_object_agg(c.""CompanyName"", c.""ContactName"") AS ""Companies""
FROM ""Customers"" AS c
GROUP BY c.""City""");
}
#endregion JsonObjectAgg
#region Statistics
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task StandardDeviation(bool async)
{
await using var ctx = CreateContext();
var query = ctx.Set<OrderDetail>()
.GroupBy(od => od.ProductID)
.Select(g => new
{
ProductID = g.Key,
SampleStandardDeviation = EF.Functions.StandardDeviationSample(g.Select(od => od.UnitPrice)),
PopulationStandardDeviation = EF.Functions.StandardDeviationPopulation(g.Select(od => od.UnitPrice))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var product9 = results.Single(r => r.ProductID == 9);
Assert.Equal(8.675943752699023, product9.SampleStandardDeviation.Value, 5);
Assert.Equal(7.759999999999856, product9.PopulationStandardDeviation.Value, 5);
AssertSql(
@"SELECT o.""ProductID"", stddev_samp(o.""UnitPrice"") AS ""SampleStandardDeviation"", stddev_pop(o.""UnitPrice"") AS ""PopulationStandardDeviation""
FROM ""Order Details"" AS o
GROUP BY o.""ProductID""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Variance(bool async)
{
await using var ctx = CreateContext();
var query = ctx.Set<OrderDetail>()
.GroupBy(od => od.ProductID)
.Select(
g => new
{
ProductID = g.Key,
SampleStandardDeviation = EF.Functions.VarianceSample(g.Select(od => od.UnitPrice)),
PopulationStandardDeviation = EF.Functions.VariancePopulation(g.Select(od => od.UnitPrice))
});
var results = async
? await query.ToListAsync()
: query.ToList();
var product9 = results.Single(r => r.ProductID == 9);
Assert.Equal(75.2719999999972, product9.SampleStandardDeviation.Value, 5);
Assert.Equal(60.217599999997766, product9.PopulationStandardDeviation.Value, 5);
AssertSql(
@"SELECT o.""ProductID"", var_samp(o.""UnitPrice"") AS ""SampleStandardDeviation"", var_pop(o.""UnitPrice"") AS ""PopulationStandardDeviation""
FROM ""Order Details"" AS o
GROUP BY o.""ProductID""");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Other_statistics_functions(bool async)
{
await using var ctx = CreateContext();
var query = ctx.Set<OrderDetail>()
.GroupBy(od => od.ProductID)
.Select(g => new
{
ProductID = g.Key,
Correlation = EF.Functions.Correlation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
CovariancePopulation = EF.Functions.CovariancePopulation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
CovarianceSample = EF.Functions.CovarianceSample(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrAverageX = EF.Functions.RegrAverageX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrAverageY = EF.Functions.RegrAverageY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrCount = EF.Functions.RegrCount(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrIntercept = EF.Functions.RegrIntercept(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrR2 = EF.Functions.RegrR2(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrSlope = EF.Functions.RegrSlope(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrSXX = EF.Functions.RegrSXX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
RegrSXY = EF.Functions.RegrSXY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))),
});
var results = async
? await query.ToListAsync()
: query.ToList();
var product9 = results.Single(r => r.ProductID == 9);
Assert.Equal(0.9336470941441423, product9.Correlation.Value, 5);
Assert.Equal(1.4799999967217445, product9.CovariancePopulation.Value, 5);
Assert.Equal(1.8499999959021807, product9.CovarianceSample.Value, 5);
Assert.Equal(0.10000000149011612, product9.RegrAverageX.Value, 5);
Assert.Equal(19, product9.RegrAverageY.Value, 5);
Assert.Equal(5, product9.RegrCount.Value);
Assert.Equal(2.5555555647538144, product9.RegrIntercept.Value, 5);
Assert.Equal(0.871696896403801, product9.RegrR2.Value, 5);
Assert.Equal(164.44444190204874, product9.RegrSlope.Value, 5);
Assert.Equal(0.045000000596046474, product9.RegrSXX.Value, 5);
Assert.Equal(7.399999983608723, product9.RegrSXY.Value, 5);
AssertSql(
@"SELECT o.""ProductID"", corr(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""Correlation"", covar_pop(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""CovariancePopulation"", covar_samp(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""CovarianceSample"", regr_avgx(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrAverageX"", regr_avgy(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrAverageY"", regr_count(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrCount"", regr_intercept(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrIntercept"", regr_r2(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrR2"", regr_slope(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrSlope"", regr_sxx(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrSXX"", regr_sxy(o.""Quantity""::double precision, o.""Discount""::double precision) AS ""RegrSXY""
FROM ""Order Details"" AS o
GROUP BY o.""ProductID""");
}
#endregion Statistics
#region Unsupported
// PostgreSQL does not have strpos with starting position
......
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
public class NorthwindGroupByQueryNpgsqlTest : NorthwindGroupByQueryRelationalTestBase<NorthwindQueryNpgsqlFixture<NoopModelCustomizer>>
......@@ -3144,6 +3146,23 @@ public override async Task GroupBy_complex_key_without_aggregate(bool async)
ORDER BY t0.""Key"" NULLS FIRST, t1.""OrderID"" NULLS FIRST");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_OrderBy_Average(bool async)
{
await AssertQueryScalar(
async,
ss => from o in ss.Set<Order>()
group o by new { o.CustomerID }
into g
select g.OrderBy(e => e.OrderID).Select(e => (int?)e.OrderID).Average());
AssertSql(
@"SELECT avg(o.""OrderID""::double precision ORDER BY o.""OrderID"" NULLS FIRST)
FROM ""Orders"" AS o
GROUP BY o.""CustomerID""");
}
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
......
......@@ -223,6 +223,31 @@ public void Union()
LIMIT 2");
}
[ConditionalFact]
[MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14
public void Union_aggregate()
{
using var context = CreateContext();
var union = context.RangeTestEntities
.Where(x => x.Id == 1 || x.Id == 2)
.GroupBy(x => true)
.Select(g => EF.Functions.RangeAgg(g.Select(x => x.IntRange)))
.Single();
Assert.Equal(new NpgsqlRange<int>[] { new(1, true, 16, false) }, union);
AssertSql(
@"SELECT range_agg(t.""IntRange"")
FROM (
SELECT r.""IntRange"", TRUE AS ""Key""
FROM ""RangeTestEntities"" AS r
WHERE r.""Id"" IN (1, 2)
) AS t
GROUP BY t.""Key""
LIMIT 2");
}
[ConditionalFact]
public void Intersect()
{
......@@ -240,6 +265,31 @@ public void Intersect()
LIMIT 2");
}
[ConditionalFact]
[MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14
public void Intersect_aggregate()
{
using var context = CreateContext();
var intersection = context.RangeTestEntities
.Where(x => x.Id == 1 || x.Id == 2)
.GroupBy(x => true)
.Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.IntRange)))
.Single();
Assert.Equal(new NpgsqlRange<int>(5, true, 11, false), intersection);
AssertSql(
@"SELECT range_intersect_agg(t.""IntRange"")
FROM (
SELECT r.""IntRange"", TRUE AS ""Key""
FROM ""RangeTestEntities"" AS r
WHERE r.""Id"" IN (1, 2)
) AS t
GROUP BY t.""Key""
LIMIT 2");
}
[ConditionalFact]
public void Except()
{
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册