From baee41c3a7dc8a099f1ad5cc11dc808588667b6d Mon Sep 17 00:00:00 2001 From: Denis Ivanov Date: Fri, 2 Jan 2026 21:17:19 +0200 Subject: [PATCH 1/2] Add `any` support --- ...lickHouseAggregateDbFunctionsExtensions.cs | 32 +++++++++ ...HouseQueryableAggregateMethodTranslator.cs | 32 +++++++++ ...gateDbFunctionsExtensionsClickHouseTest.cs | 65 +++++++++++++++++++ ...HouseAggregateDbFunctionsExtensionsTest.cs | 18 +++++ 4 files changed, 147 insertions(+) create mode 100644 src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs create mode 100644 test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs create mode 100644 test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs diff --git a/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs b/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs new file mode 100644 index 0000000..92b8ff1 --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +public static class ClickHouseAggregateDbFunctionsExtensions +{ + /// + /// Returns any value of the column. + /// + /// DbFunctions instance. + /// The column. + /// The type of the column. + /// Any value of the column. + public static T Any(this DbFunctions _, IEnumerable column) + { + throw new InvalidOperationException(); + } + + /// + /// Returns any value of the column, respecting nulls. + /// + /// DbFunctions instance. + /// The column. + /// The type of the column. + /// Any value of the column. + public static T AnyRespectNulls(this DbFunctions _, IEnumerable column) + { + throw new InvalidOperationException(); + } +} diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs index 8c76761..ad6c446 100644 --- a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs @@ -118,6 +118,38 @@ public ClickHouseQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExp } } + if (method.DeclaringType == typeof(ClickHouseAggregateDbFunctionsExtensions)) + { + var methodInfo = method.IsGenericMethod + ? method.GetGenericMethodDefinition() + : method; + + switch (methodInfo.Name) + { + case nameof(ClickHouseAggregateDbFunctionsExtensions.Any) + when source.Selector is SqlExpression anySqlExpression: + anySqlExpression = CombineTerms(source, anySqlExpression); + return _sqlExpressionFactory.Function( + "any", + [anySqlExpression], + nullable: true, + argumentsPropagateNullability: [false], + anySqlExpression.Type, + anySqlExpression.TypeMapping); + + case nameof(ClickHouseAggregateDbFunctionsExtensions.AnyRespectNulls) + when source.Selector is SqlExpression anyRespectNullsSqlExpression: + anyRespectNullsSqlExpression = CombineTerms(source, anyRespectNullsSqlExpression); + return _sqlExpressionFactory.Function( + "anyRespectNulls", + [anyRespectNullsSqlExpression], + nullable: true, + argumentsPropagateNullability: [false], + anyRespectNullsSqlExpression.Type, + anyRespectNullsSqlExpression.TypeMapping); + } + } + return null; } diff --git a/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs b/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs new file mode 100644 index 0000000..81fc42b --- /dev/null +++ b/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.TestUtilities; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.ClickHouse.FunctionalTests.Query; + +public class AggregateDbFunctionsExtensionsClickHouseTest : IClassFixture> +{ + public AggregateDbFunctionsExtensionsClickHouseTest( + NorthwindQueryClickHouseFixture fixture, + ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [Fact] + public async Task Any() + { + var context = CreateContext(); + + await context.Customers + .GroupBy(c => c.Country) + .Select(g => new { Key = g.Key, AnyCity = EF.Functions.Any(g.Select(c => c.City)) }) + .ToListAsync(); + + AssertSql( + """ + SELECT "c"."Country" AS "Key", any("c"."City") AS "AnyCity" + FROM "Customers" AS "c" + GROUP BY "c"."Country" + """); + } + + [Fact] + public async Task AnyRespectNulls() + { + var context = CreateContext(); + + await context.Customers + .GroupBy(c => c.Country) + .Select(g => new { Key = g.Key, AnyRegion = EF.Functions.AnyRespectNulls(g.Select(c => c.Region)) }) + .ToListAsync(); + + AssertSql( + """ + SELECT "c"."Country" AS "Key", anyRespectNulls("c"."Region") AS "AnyRegion" + FROM "Customers" AS "c" + GROUP BY "c"."Country" + """); + } + + protected NorthwindQueryClickHouseFixture Fixture { get; } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected NorthwindContext CreateContext() + => Fixture.CreateContext(); +} diff --git a/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs new file mode 100644 index 0000000..bd9ff7a --- /dev/null +++ b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.ClickHouse.Tests.Extensions.DbFunctionsExtensions; + +public sealed class ClickHouseAggregateDbFunctionsExtensionsTest +{ + [Fact] + public void Any_ThrowsException() + { + Assert.Throws(() => EF.Functions.Any("test")); + } + + [Fact] + public void AnyRespectNulls_ThrowsException() + { + Assert.Throws(() => EF.Functions.AnyRespectNulls("test")); + } +} From 79c7050d7e162c5cb5c4f1bc4041a397cbd54c7a Mon Sep 17 00:00:00 2001 From: Denis Ivanov Date: Fri, 23 Jan 2026 13:44:59 +0200 Subject: [PATCH 2/2] Try add ClickHouse aggregate functions support --- .../ClickHouseServiceCollectionExtensions.cs | 1 + ...lickHouseAggregateDbFunctionsExtensions.cs | 32 --------------- .../ClickHouseEnumerable.cs | 18 +++++++++ ...seAggregateMethodCallTranslatorProvider.cs | 5 ++- .../ClickHouseAggregateMethodTranslator.cs | 40 +++++++++++++++++++ .../ClickHouseAggregateMethodVisitor.cs | 18 +++++++++ .../ClickHouseQueryTranslationPreprocessor.cs | 21 ++++++++++ ...ouseQueryTranslationPreprocessorFactory.cs | 19 +++++++++ ...HouseQueryableAggregateMethodTranslator.cs | 32 --------------- ...gateDbFunctionsExtensionsClickHouseTest.cs | 6 +-- ...onsTest.cs => ClickHouseEnumerableTest.cs} | 2 +- 11 files changed, 125 insertions(+), 69 deletions(-) delete mode 100644 src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs create mode 100644 src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseEnumerable.cs create mode 100644 src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodTranslator.cs create mode 100644 src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodVisitor.cs create mode 100644 src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessor.cs create mode 100644 src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessorFactory.cs rename test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/{ClickHouseAggregateDbFunctionsExtensionsTest.cs => ClickHouseEnumerableTest.cs} (87%) diff --git a/src/EntityFrameworkCore.ClickHouse/Extensions/ClickHouseServiceCollectionExtensions.cs b/src/EntityFrameworkCore.ClickHouse/Extensions/ClickHouseServiceCollectionExtensions.cs index 73b3f25..9e68ddf 100644 --- a/src/EntityFrameworkCore.ClickHouse/Extensions/ClickHouseServiceCollectionExtensions.cs +++ b/src/EntityFrameworkCore.ClickHouse/Extensions/ClickHouseServiceCollectionExtensions.cs @@ -53,6 +53,7 @@ public static IServiceCollection AddEntityFrameworkClickHouse([NotNull] this ISe .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs b/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs deleted file mode 100644 index 92b8ff1..0000000 --- a/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -public static class ClickHouseAggregateDbFunctionsExtensions -{ - /// - /// Returns any value of the column. - /// - /// DbFunctions instance. - /// The column. - /// The type of the column. - /// Any value of the column. - public static T Any(this DbFunctions _, IEnumerable column) - { - throw new InvalidOperationException(); - } - - /// - /// Returns any value of the column, respecting nulls. - /// - /// DbFunctions instance. - /// The column. - /// The type of the column. - /// Any value of the column. - public static T AnyRespectNulls(this DbFunctions _, IEnumerable column) - { - throw new InvalidOperationException(); - } -} diff --git a/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseEnumerable.cs b/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseEnumerable.cs new file mode 100644 index 0000000..13d811a --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Extensions/DbFunctionsExtensions/ClickHouseEnumerable.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +public static class ClickHouseEnumerable +{ + public static TResult Any(this IEnumerable source, Func selector) + { + throw new InvalidOperationException(); + } + + public static TResult? AnyRespectNulls(this IEnumerable source, Func selector) + { + throw new InvalidOperationException(); + } +} diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodCallTranslatorProvider.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodCallTranslatorProvider.cs index eb76fa9..a4c427c 100644 --- a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodCallTranslatorProvider.cs +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodCallTranslatorProvider.cs @@ -6,6 +6,9 @@ public class ClickHouseAggregateMethodCallTranslatorProvider : RelationalAggrega { public ClickHouseAggregateMethodCallTranslatorProvider(RelationalAggregateMethodCallTranslatorProviderDependencies dependencies) : base(dependencies) { - AddTranslators([new ClickHouseQueryableAggregateMethodTranslator(dependencies.SqlExpressionFactory)]); + AddTranslators([ + new ClickHouseAggregateMethodTranslator(dependencies.SqlExpressionFactory), + new ClickHouseQueryableAggregateMethodTranslator(dependencies.SqlExpressionFactory) + ]); } } diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodTranslator.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodTranslator.cs new file mode 100644 index 0000000..45b804e --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodTranslator.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace ClickHouse.EntityFrameworkCore.Query.Internal; + +public class ClickHouseAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + public ClickHouseAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + public SqlExpression? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(ClickHouseEnumerable)) + { + switch (method.Name) + { + case nameof(ClickHouseEnumerable.Any): + throw new NotImplementedException(); + + case nameof(ClickHouseEnumerable.AnyRespectNulls): + throw new NotImplementedException(); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodVisitor.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodVisitor.cs new file mode 100644 index 0000000..80453c6 --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseAggregateMethodVisitor.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using System.Linq.Expressions; + +namespace ClickHouse.EntityFrameworkCore.Query.Internal; + +public class ClickHouseAggregateMethodVisitor : ExpressionVisitor +{ + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(ClickHouseEnumerable) && node.Arguments.Count > 1) + { + return new EnumerableExpression(Visit(node.Arguments[1])); + } + + return base.VisitMethodCall(node); + } +} diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessor.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessor.cs new file mode 100644 index 0000000..86c596a --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessor.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Query; +using System.Linq.Expressions; + +namespace ClickHouse.EntityFrameworkCore.Query.Internal; + +public class ClickHouseQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor +{ + public ClickHouseQueryTranslationPreprocessor( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + } + + public override Expression Process(Expression query) + { + query = new ClickHouseAggregateMethodVisitor().Visit(query); + + return base.Process(query); + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessorFactory.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessorFactory.cs new file mode 100644 index 0000000..2afea56 --- /dev/null +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryTranslationPreprocessorFactory.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; + +namespace ClickHouse.EntityFrameworkCore.Query.Internal; + +public class ClickHouseQueryTranslationPreprocessorFactory : RelationalQueryTranslationPreprocessorFactory +{ + public ClickHouseQueryTranslationPreprocessorFactory( + QueryTranslationPreprocessorDependencies dependencies, + RelationalQueryTranslationPreprocessorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + public override QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) + { + return new ClickHouseQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext); + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs index ad6c446..8c76761 100644 --- a/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs +++ b/src/EntityFrameworkCore.ClickHouse/Query/Internal/ClickHouseQueryableAggregateMethodTranslator.cs @@ -118,38 +118,6 @@ public ClickHouseQueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExp } } - if (method.DeclaringType == typeof(ClickHouseAggregateDbFunctionsExtensions)) - { - var methodInfo = method.IsGenericMethod - ? method.GetGenericMethodDefinition() - : method; - - switch (methodInfo.Name) - { - case nameof(ClickHouseAggregateDbFunctionsExtensions.Any) - when source.Selector is SqlExpression anySqlExpression: - anySqlExpression = CombineTerms(source, anySqlExpression); - return _sqlExpressionFactory.Function( - "any", - [anySqlExpression], - nullable: true, - argumentsPropagateNullability: [false], - anySqlExpression.Type, - anySqlExpression.TypeMapping); - - case nameof(ClickHouseAggregateDbFunctionsExtensions.AnyRespectNulls) - when source.Selector is SqlExpression anyRespectNullsSqlExpression: - anyRespectNullsSqlExpression = CombineTerms(source, anyRespectNullsSqlExpression); - return _sqlExpressionFactory.Function( - "anyRespectNulls", - [anyRespectNullsSqlExpression], - nullable: true, - argumentsPropagateNullability: [false], - anyRespectNullsSqlExpression.Type, - anyRespectNullsSqlExpression.TypeMapping); - } - } - return null; } diff --git a/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs b/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs index 81fc42b..8de1d0b 100644 --- a/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs +++ b/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs @@ -23,10 +23,10 @@ public AggregateDbFunctionsExtensionsClickHouseTest( public async Task Any() { var context = CreateContext(); - + await context.Customers .GroupBy(c => c.Country) - .Select(g => new { Key = g.Key, AnyCity = EF.Functions.Any(g.Select(c => c.City)) }) + .Select(g => new { Key = g.Key, AnyCity = g.Any(e => e.City) }) .ToListAsync(); AssertSql( @@ -44,7 +44,7 @@ public async Task AnyRespectNulls() await context.Customers .GroupBy(c => c.Country) - .Select(g => new { Key = g.Key, AnyRegion = EF.Functions.AnyRespectNulls(g.Select(c => c.Region)) }) + .Select(g => new { Key = g.Key, AnyRegion = g.AnyRespectNulls(e => e.Region) }) .ToListAsync(); AssertSql( diff --git a/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseEnumerableTest.cs similarity index 87% rename from test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs rename to test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseEnumerableTest.cs index bd9ff7a..385e874 100644 --- a/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseAggregateDbFunctionsExtensionsTest.cs +++ b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseEnumerableTest.cs @@ -2,7 +2,7 @@ namespace EntityFrameworkCore.ClickHouse.Tests.Extensions.DbFunctionsExtensions; -public sealed class ClickHouseAggregateDbFunctionsExtensionsTest +public sealed class ClickHouseEnumerableTest { [Fact] public void Any_ThrowsException()