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/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/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs b/test/EntityFrameworkCore.ClickHouse.FunctionalTests/Query/AggregateDbFunctionsExtensionsClickHouseTest.cs new file mode 100644 index 0000000..8de1d0b --- /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 = g.Any(e => e.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 = g.AnyRespectNulls(e => e.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/ClickHouseEnumerableTest.cs b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseEnumerableTest.cs new file mode 100644 index 0000000..385e874 --- /dev/null +++ b/test/EntityFrameworkCore.ClickHouse.Tests/Extensions/DbFunctionsExtensions/ClickHouseEnumerableTest.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.ClickHouse.Tests.Extensions.DbFunctionsExtensions; + +public sealed class ClickHouseEnumerableTest +{ + [Fact] + public void Any_ThrowsException() + { + Assert.Throws(() => EF.Functions.Any("test")); + } + + [Fact] + public void AnyRespectNulls_ThrowsException() + { + Assert.Throws(() => EF.Functions.AnyRespectNulls("test")); + } +}