From a95017e38b727b8a70dd312b0fe75ec5651cce85 Mon Sep 17 00:00:00 2001 From: Laurents Meyer Date: Fri, 27 Oct 2023 22:27:30 +0200 Subject: [PATCH] Auto skip statements that are not supported by Jet. Track unsupported statements in log files. (#169) --- .../JetCompatibilityExpressionVisitor.cs | 51 +++++++++++++++++++ .../Internal/JetParameterBasedSqlProcessor.cs | 10 ++-- .../JetParameterBasedSqlProcessorFactory.cs | 3 +- .../Sql/Internal/JetQuerySqlGenerator.cs | 9 ++-- .../TestUtilities/Xunit/JetXunitTestRunner.cs | 26 ++++++---- 5 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs b/src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs new file mode 100644 index 0000000..675be49 --- /dev/null +++ b/src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs @@ -0,0 +1,51 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.Jet.Query.Internal; + +public class JetCompatibilityExpressionVisitor : ExpressionVisitor +{ + protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch + { + RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression), + CrossApplyExpression crossApplyExpression => VisitCrossApply(crossApplyExpression), + OuterApplyExpression outerApplyExpression => VisitOuterApply(outerApplyExpression), + ExceptExpression exceptExpression => VisitExcept(exceptExpression), + IntersectExpression intersectExpression => VisitIntersect(intersectExpression), + JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), + + ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.Update(Visit(shapedQueryExpression.QueryExpression), Visit(shapedQueryExpression.ShaperExpression)), + _ => base.VisitExtension(extensionExpression) + }; + + protected virtual Expression VisitRowNumber(RowNumberExpression rowNumberExpression) + => TranslationFailed(rowNumberExpression); + + protected virtual Expression VisitCrossApply(CrossApplyExpression crossApplyExpression) + => TranslationFailed(crossApplyExpression); + + protected virtual Expression VisitOuterApply(OuterApplyExpression outerApplyExpression) + => TranslationFailed(outerApplyExpression); + + protected virtual Expression VisitExcept(ExceptExpression exceptExpression) + => TranslationFailed(exceptExpression); + + protected virtual Expression VisitIntersect(IntersectExpression intersectExpression) + => TranslationFailed(intersectExpression); + + protected virtual Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + => jsonScalarExpression.Path.Count > 0 + ? TranslationFailed(jsonScalarExpression) + : jsonScalarExpression; + + protected virtual Expression TranslationFailed(Expression expression) + => throw new InvalidOperationException(CoreStrings.TranslationFailed(expression.Print())); +} \ No newline at end of file diff --git a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs index b406815..858f59f 100644 --- a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs +++ b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq.Expressions; -using EntityFrameworkCore.Jet.Query.Internal; using EntityFrameworkCore.Jet.Utilities; using Microsoft.EntityFrameworkCore.Query; @@ -41,14 +40,19 @@ public class JetParameterBasedSqlProcessor : RelationalParameterBasedSqlProcesso IReadOnlyDictionary parametersValues, out bool canCache) { - var optimizedQueryExpression = base.Optimize(queryExpression, parametersValues, out canCache); + queryExpression = base.Optimize(queryExpression, parametersValues, out canCache); /*optimizedQueryExpression = new SkipTakeCollapsingExpressionVisitor(Dependencies.SqlExpressionFactory) .Process(optimizedQueryExpression, parametersValues, out var canCache2);*/ //canCache &= canCache2; - return new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression); + queryExpression = new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(queryExpression); + + // Run the compatibility checks as late in the query pipeline (before the actual SQL translation happens) as reasonable. + queryExpression = new JetCompatibilityExpressionVisitor().Visit(queryExpression); + + return queryExpression; } /// diff --git a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessorFactory.cs b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessorFactory.cs index 8e88239..2ab151e 100644 --- a/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.Jet/Query/Internal/JetParameterBasedSqlProcessorFactory.cs @@ -19,8 +19,7 @@ public class JetParameterBasedSqlProcessorFactory : IRelationalParameterBasedSql /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public JetParameterBasedSqlProcessorFactory( - RelationalParameterBasedSqlProcessorDependencies dependencies) + public JetParameterBasedSqlProcessorFactory(RelationalParameterBasedSqlProcessorDependencies dependencies) { Dependencies = dependencies; } diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs index 059401d..d63b8f4 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs +++ b/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using EntityFrameworkCore.Jet.Data; using System.Linq; @@ -141,8 +142,7 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal if (isApplyExpression) { - throw new InvalidOperationException( - "Jet does not support APPLY statements. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); + throw new UnreachableException(); } if (index > 0) @@ -379,7 +379,8 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal Visit(jsonScalarExpression.Json); return jsonScalarExpression; } - return base.VisitJsonScalar(jsonScalarExpression); + + throw new UnreachableException(); } private bool IsNonComposedSetOperation(SelectExpression selectExpression) @@ -826,6 +827,6 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal } protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) - => throw new InvalidOperationException(CoreStrings.TranslationFailed(rowNumberExpression)); + => throw new UnreachableException(); } } \ No newline at end of file diff --git a/test/Shared/TestUtilities/Xunit/JetXunitTestRunner.cs b/test/Shared/TestUtilities/Xunit/JetXunitTestRunner.cs index 4d7a0c3..71c0ee4 100644 --- a/test/Shared/TestUtilities/Xunit/JetXunitTestRunner.cs +++ b/test/Shared/TestUtilities/Xunit/JetXunitTestRunner.cs @@ -152,7 +152,11 @@ public class JetXunitTestRunner : XunitTestRunner else if (message.StartsWith("The LINQ expression '") && message.Contains("' could not be translated.")) { - var expectedUnsupportedTranslation = message.Contains("RowNumberExpression"); + var expectedUnsupportedTranslation = message.Contains("OUTER APPLY") || + message.Contains("CROSS APPLY") || + message.Contains("ROW_NUMBER() OVER") || + message.Contains("EXCEPT") || + message.Contains("INTERSECT"); skip = expectedUnsupportedTranslation; unexpectedUnsupportedTranslation = !expectedUnsupportedTranslation; @@ -160,22 +164,22 @@ public class JetXunitTestRunner : XunitTestRunner if (skip) { - // var sb = new StringBuilder(); - // sb.AppendLine(message.ReplaceLineEndings(" ")); - // sb.AppendLine("-----"); - // - // File.AppendAllText("ExpectedUnsupportedTranslations.txt", sb.ToString()); + var sb = new StringBuilder(); + sb.AppendLine(message.ReplaceLineEndings(" ")); + sb.AppendLine("-----"); + + File.AppendAllText("ExpectedUnsupportedTranslations.txt", sb.ToString()); break; } if (unexpectedUnsupportedTranslation) { - // var sb = new StringBuilder(); - // sb.AppendLine(message.ReplaceLineEndings(" ")); - // sb.AppendLine("-----"); - // - // File.AppendAllText("UnsupportedTranslations.txt", sb.ToString()); + var sb = new StringBuilder(); + sb.AppendLine(message.ReplaceLineEndings(" ")); + sb.AppendLine("-----"); + + File.AppendAllText("UnexpectedUnsupportedTranslations.txt", sb.ToString()); } } }