Skip to content

Commit ae33d23

Browse files
committed
Optimize LongCount() > 0 to Any() for Jet queries
Introduce JetQueryTranslationPreprocessor and factory to rewrite LongCount() > 0 as Any(), ensuring Jet/Access SQL compatibility. Update DI registration and test assertions to use EXISTS instead of COUNT_BIG.
1 parent 0685104 commit ae33d23

9 files changed

Lines changed: 119 additions & 42 deletions

src/EFCore.Jet/Extensions/JetServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static IServiceCollection AddEntityFrameworkJet(this IServiceCollection s
5353
.TryAdd<IQuerySqlGeneratorFactory, JetQuerySqlGeneratorFactory>()
5454
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, JetSqlTranslatingExpressionVisitorFactory>()
5555
.TryAdd<ISqlExpressionFactory, JetSqlExpressionFactory>()
56+
.TryAdd<IQueryTranslationPreprocessorFactory, JetQueryTranslationPreprocessorFactory>()
5657
.TryAdd<IQueryTranslationPostprocessorFactory, JetQueryTranslationPostprocessorFactory>()
5758
.TryAdd<IRelationalTransactionFactory, JetTransactionFactory>()
5859
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, JetParameterBasedSqlProcessorFactory>()
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace EntityFrameworkCore.Jet.Query.Internal;
4+
5+
/// <summary>
6+
/// Extends EF Core's query translation preprocessing with Jet-specific optimizations.
7+
/// Currently adds <c>LongCount() &gt; 0 → Any()</c> which mirrors EF Core's built-in
8+
/// <c>Count() &gt; 0 → Any()</c> optimization that is missing for the long variant.
9+
/// </summary>
10+
public class JetQueryTranslationPreprocessor(
11+
QueryTranslationPreprocessorDependencies dependencies,
12+
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
13+
RelationalQueryCompilationContext queryCompilationContext)
14+
: RelationalQueryTranslationPreprocessor(dependencies, relationalDependencies, queryCompilationContext)
15+
{
16+
public override Expression Process(Expression query)
17+
{
18+
query = base.Process(query);
19+
query = new LongCountToAnyVisitor().Visit(query);
20+
return query;
21+
}
22+
23+
private sealed class LongCountToAnyVisitor : ExpressionVisitor
24+
{
25+
protected override Expression VisitBinary(BinaryExpression binaryExpression)
26+
{
27+
var result = base.VisitBinary(binaryExpression);
28+
if (result is not BinaryExpression { NodeType: ExpressionType.GreaterThan } binary)
29+
return result;
30+
31+
if (binary.Left is MethodCallExpression { Method.IsGenericMethod: true } longCountCall
32+
&& IsZeroConstant(binary.Right)
33+
&& IsLongCountMethod(longCountCall.Method.GetGenericMethodDefinition()))
34+
{
35+
return MakeAnyCall(longCountCall);
36+
}
37+
38+
return result;
39+
}
40+
41+
private static bool IsZeroConstant(Expression expr)
42+
{
43+
if (expr is not ConstantExpression { Value: var val })
44+
return false;
45+
return val is int i && i == 0 || val is long l && l == 0;
46+
}
47+
48+
private static bool IsLongCountMethod(MethodInfo method)
49+
=> method == QueryableMethods.LongCountWithoutPredicate
50+
|| method == QueryableMethods.LongCountWithPredicate;
51+
52+
private static Expression MakeAnyCall(MethodCallExpression longCountCall)
53+
{
54+
var elementType = longCountCall.Method.GetGenericArguments()[0];
55+
var anyMethod = longCountCall.Arguments.Count == 2
56+
? QueryableMethods.AnyWithPredicate.MakeGenericMethod(elementType)
57+
: QueryableMethods.AnyWithoutPredicate.MakeGenericMethod(elementType);
58+
return Expression.Call(anyMethod, longCountCall.Arguments);
59+
}
60+
}
61+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace EntityFrameworkCore.Jet.Query.Internal;
4+
5+
public class JetQueryTranslationPreprocessorFactory(
6+
QueryTranslationPreprocessorDependencies dependencies,
7+
RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
8+
: IQueryTranslationPreprocessorFactory
9+
{
10+
public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
11+
=> new JetQueryTranslationPreprocessor(
12+
dependencies,
13+
relationalDependencies,
14+
(RelationalQueryCompilationContext)queryCompilationContext);
15+
}

test/EFCore.Jet.FunctionalTests/Query/ManyToManyNoTrackingQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
126126

127127
AssertSql(
128128
"""
129-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
130-
FROM [EntityTwos] AS [e]
131-
WHERE (
132-
SELECT COUNT_BIG(*)
133-
FROM [JoinTwoToThree] AS [j]
134-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
135-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
129+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
130+
FROM `EntityTwos` AS `e`
131+
WHERE EXISTS (
132+
SELECT 1
133+
FROM `JoinTwoToThree` AS `j`
134+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
135+
WHERE `e`.`Id` = `j`.`TwoId`)
136136
""");
137137
}
138138

test/EFCore.Jet.FunctionalTests/Query/ManyToManyQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
126126

127127
AssertSql(
128128
"""
129-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
130-
FROM [EntityTwos] AS [e]
131-
WHERE (
132-
SELECT COUNT_BIG(*)
133-
FROM [JoinTwoToThree] AS [j]
134-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
135-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
129+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
130+
FROM `EntityTwos` AS `e`
131+
WHERE EXISTS (
132+
SELECT 1
133+
FROM `JoinTwoToThree` AS `j`
134+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
135+
WHERE `e`.`Id` = `j`.`TwoId`)
136136
""");
137137
}
138138

test/EFCore.Jet.FunctionalTests/Query/TPCManyToManyNoTrackingQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
134134

135135
AssertSql(
136136
"""
137-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
138-
FROM [EntityTwos] AS [e]
139-
WHERE (
140-
SELECT COUNT_BIG(*)
141-
FROM [JoinTwoToThree] AS [j]
142-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
143-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
137+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
138+
FROM `EntityTwos` AS `e`
139+
WHERE EXISTS (
140+
SELECT 1
141+
FROM `JoinTwoToThree` AS `j`
142+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
143+
WHERE `e`.`Id` = `j`.`TwoId`)
144144
""");
145145
}
146146

test/EFCore.Jet.FunctionalTests/Query/TPCManyToManyQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
134134

135135
AssertSql(
136136
"""
137-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
138-
FROM [EntityTwos] AS [e]
139-
WHERE (
140-
SELECT COUNT_BIG(*)
141-
FROM [JoinTwoToThree] AS [j]
142-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
143-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
137+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
138+
FROM `EntityTwos` AS `e`
139+
WHERE EXISTS (
140+
SELECT 1
141+
FROM `JoinTwoToThree` AS `j`
142+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
143+
WHERE `e`.`Id` = `j`.`TwoId`)
144144
""");
145145
}
146146

test/EFCore.Jet.FunctionalTests/Query/TPTManyToManyNoTrackingQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
133133

134134
AssertSql(
135135
"""
136-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
137-
FROM [EntityTwos] AS [e]
138-
WHERE (
139-
SELECT COUNT_BIG(*)
140-
FROM [JoinTwoToThree] AS [j]
141-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
142-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
136+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
137+
FROM `EntityTwos` AS `e`
138+
WHERE EXISTS (
139+
SELECT 1
140+
FROM `JoinTwoToThree` AS `j`
141+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
142+
WHERE `e`.`Id` = `j`.`TwoId`)
143143
""");
144144
}
145145

test/EFCore.Jet.FunctionalTests/Query/TPTManyToManyQueryJetTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ public override async Task Skip_navigation_long_count_without_predicate(bool asy
132132

133133
AssertSql(
134134
"""
135-
SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[ReferenceInverseId]
136-
FROM [EntityTwos] AS [e]
137-
WHERE (
138-
SELECT COUNT_BIG(*)
139-
FROM [JoinTwoToThree] AS [j]
140-
INNER JOIN [EntityThrees] AS [e0] ON [j].[ThreeId] = [e0].[Id]
141-
WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)
135+
SELECT `e`.`Id`, `e`.`CollectionInverseId`, `e`.`ExtraId`, `e`.`Name`, `e`.`ReferenceInverseId`
136+
FROM `EntityTwos` AS `e`
137+
WHERE EXISTS (
138+
SELECT 1
139+
FROM `JoinTwoToThree` AS `j`
140+
INNER JOIN `EntityThrees` AS `e0` ON `j`.`ThreeId` = `e0`.`Id`
141+
WHERE `e`.`Id` = `j`.`TwoId`)
142142
""");
143143
}
144144

0 commit comments

Comments
 (0)