diff --git a/src/EntityFramework/Core/Common/CommandTrees/DbCommandTree.cs b/src/EntityFramework/Core/Common/CommandTrees/DbCommandTree.cs index 7e04b44679..c09673ef9e 100644 --- a/src/EntityFramework/Core/Common/CommandTrees/DbCommandTree.cs +++ b/src/EntityFramework/Core/Common/CommandTrees/DbCommandTree.cs @@ -17,6 +17,7 @@ public abstract class DbCommandTree private readonly MetadataWorkspace _metadata; private readonly DataSpace _dataSpace; private readonly bool _useDatabaseNullSemantics; + private readonly bool _disableFilterOverProjectionSimplificationForCustomFunctions; internal DbCommandTree() { @@ -30,7 +31,7 @@ internal DbCommandTree() // The logical 'space' that metadata in the expressions used in this command tree must belong to. // A boolean that indicates whether database null semantics are exhibited when comparing // two operands, both of which are potentially nullable. The default value is true. - internal DbCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, bool useDatabaseNullSemantics = true) + internal DbCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, bool useDatabaseNullSemantics = true, bool disableFilterOverProjectionSimplificationForCustomFunctions = false) { // Ensure the metadata workspace is non-null DebugCheck.NotNull(metadata); @@ -44,6 +45,7 @@ internal DbCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, bool use _metadata = metadata; _dataSpace = dataSpace; _useDatabaseNullSemantics = useDatabaseNullSemantics; + _disableFilterOverProjectionSimplificationForCustomFunctions = disableFilterOverProjectionSimplificationForCustomFunctions; } /// @@ -68,6 +70,11 @@ public bool UseDatabaseNullSemantics get { return _useDatabaseNullSemantics; } } + public bool DisableFilterOverProjectionSimplificationForCustomFunctions + { + get { return _disableFilterOverProjectionSimplificationForCustomFunctions; } + } + /// /// Gets the name and corresponding type of each parameter that can be referenced within this /// When set to false the validation of the tree is turned off. /// A boolean that indicates whether database null semantics are exhibited when comparing /// two operands, both of which are potentially nullable. + /// A boolean that indicates whether + /// filter over projection simplification should be used. /// /// /// or @@ -39,8 +41,8 @@ public sealed class DbQueryCommandTree : DbCommandTree /// /// does not represent a valid data space /// - public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query, bool validate, bool useDatabaseNullSemantics) - : base(metadata, dataSpace, useDatabaseNullSemantics) + public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query, bool validate, bool useDatabaseNullSemantics, bool disableFilterOverProjectionSimplificationForCustomFunctions) + : base(metadata, dataSpace, useDatabaseNullSemantics, disableFilterOverProjectionSimplificationForCustomFunctions) { // Ensure the query expression is non-null Check.NotNull(query, "query"); @@ -57,6 +59,32 @@ public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExp _query = query; } + /// + /// Constructs a new DbQueryCommandTree that uses the specified metadata workspace. + /// + /// The metadata workspace that the command tree should use. + /// The logical 'space' that metadata in the expressions used in this command tree must belong to. + /// + /// A that defines the logic of the query. + /// + /// When set to false the validation of the tree is turned off. + /// A boolean that indicates whether database null semantics are exhibited when comparing + /// two operands, both of which are potentially nullable. + /// + /// + /// or + /// + /// is null + /// + /// + /// + /// does not represent a valid data space + /// + public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query, bool validate, bool useDatabaseNullSemantics) + : this(metadata, dataSpace, query, validate, useDatabaseNullSemantics, false) + { + } + /// /// Constructs a new DbQueryCommandTree that uses the specified metadata workspace, using database null semantics. /// @@ -77,7 +105,7 @@ public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExp /// does not represent a valid data space /// public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query, bool validate) - : this(metadata, dataSpace, query, validate, true) + : this(metadata, dataSpace, query, validate, true, false) { } @@ -100,7 +128,7 @@ public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExp /// does not represent a valid data space /// public DbQueryCommandTree(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query) - : this(metadata, dataSpace, query, true, true) + : this(metadata, dataSpace, query, true, true, false) { } @@ -147,7 +175,7 @@ internal override string PrintTree(ExpressionPrinter printer) } internal static DbQueryCommandTree FromValidExpression(MetadataWorkspace metadata, DataSpace dataSpace, DbExpression query, - bool useDatabaseNullSemantics) + bool useDatabaseNullSemantics, bool disableFilterOverProjectionSimplificationForCustomFunctions) { return new DbQueryCommandTree(metadata, dataSpace, query, #if DEBUG @@ -155,7 +183,8 @@ internal static DbQueryCommandTree FromValidExpression(MetadataWorkspace metadat #else false, #endif - useDatabaseNullSemantics); + useDatabaseNullSemantics, + disableFilterOverProjectionSimplificationForCustomFunctions); } } } diff --git a/src/EntityFramework/Core/Common/CommandTrees/Internal/ViewSimplifier.cs b/src/EntityFramework/Core/Common/CommandTrees/Internal/ViewSimplifier.cs index 009a857c9c..cddd619b74 100644 --- a/src/EntityFramework/Core/Common/CommandTrees/Internal/ViewSimplifier.cs +++ b/src/EntityFramework/Core/Common/CommandTrees/Internal/ViewSimplifier.cs @@ -58,7 +58,7 @@ private DbQueryCommandTree Simplify(DbQueryCommandTree view) queryExpression = simplifier(queryExpression); view = DbQueryCommandTree.FromValidExpression( - view.MetadataWorkspace, view.DataSpace, queryExpression, view.UseDatabaseNullSemantics); + view.MetadataWorkspace, view.DataSpace, queryExpression, view.UseDatabaseNullSemantics, view.DisableFilterOverProjectionSimplificationForCustomFunctions); return view; } diff --git a/src/EntityFramework/Core/Common/DbProviderServices.cs b/src/EntityFramework/Core/Common/DbProviderServices.cs index cfbc91bc0a..6630958be4 100644 --- a/src/EntityFramework/Core/Common/DbProviderServices.cs +++ b/src/EntityFramework/Core/Common/DbProviderServices.cs @@ -39,6 +39,7 @@ private static readonly ConcurrentDictionary /// Constructs an EF provider that will use the obtained from /// the app domain Singleton for resolving EF dependencies such diff --git a/src/EntityFramework/Core/Common/EntitySql/SemanticAnalyzer.cs b/src/EntityFramework/Core/Common/EntitySql/SemanticAnalyzer.cs index eeac2b810f..24c941e3bc 100644 --- a/src/EntityFramework/Core/Common/EntitySql/SemanticAnalyzer.cs +++ b/src/EntityFramework/Core/Common/EntitySql/SemanticAnalyzer.cs @@ -254,7 +254,7 @@ private static ParseResult ConvertQueryStatementToDbCommandTree(Statement astSta return new ParseResult( DbQueryCommandTree.FromValidExpression( sr.TypeResolver.Perspective.MetadataWorkspace, sr.TypeResolver.Perspective.TargetDataspace, converted, - useDatabaseNullSemantics: true), + useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false), functionDefs); } diff --git a/src/EntityFramework/Core/Mapping/FunctionImportMappingComposable.cs b/src/EntityFramework/Core/Mapping/FunctionImportMappingComposable.cs index cae83e32f8..cad9748942 100644 --- a/src/EntityFramework/Core/Mapping/FunctionImportMappingComposable.cs +++ b/src/EntityFramework/Core/Mapping/FunctionImportMappingComposable.cs @@ -403,7 +403,7 @@ internal DbQueryCommandTree GenerateFunctionView(out DiscriminatorMap discrimina // Generate parameterized command, where command parameters are semantically the c-space function parameters. return DbQueryCommandTree.FromValidExpression( _containerMapping.StorageMappingItemCollection.Workspace, TargetPerspective.TargetPerspectiveDataSpace, queryExpression, - useDatabaseNullSemantics: true); + useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false); } private IEnumerable GetParametersForTargetFunctionCall() diff --git a/src/EntityFramework/Core/Mapping/ViewGeneration/CqlGenerator.cs b/src/EntityFramework/Core/Mapping/ViewGeneration/CqlGenerator.cs index defbb4492b..7774f3c6f8 100644 --- a/src/EntityFramework/Core/Mapping/ViewGeneration/CqlGenerator.cs +++ b/src/EntityFramework/Core/Mapping/ViewGeneration/CqlGenerator.cs @@ -107,7 +107,7 @@ internal DbQueryCommandTree GenerateCqt() return DbQueryCommandTree.FromValidExpression( m_mappingItemCollection.Workspace, TargetPerspective.TargetPerspectiveDataSpace, query, - useDatabaseNullSemantics: true); + useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false); } // diff --git a/src/EntityFramework/Core/Objects/ELinq/CompiledELinqQueryState.cs b/src/EntityFramework/Core/Objects/ELinq/CompiledELinqQueryState.cs index e035c19a24..fa556a57c0 100644 --- a/src/EntityFramework/Core/Objects/ELinq/CompiledELinqQueryState.cs +++ b/src/EntityFramework/Core/Objects/ELinq/CompiledELinqQueryState.cs @@ -57,6 +57,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg ObjectQueryExecutionPlan plan = null; var cacheEntry = _cacheEntry; var useCSharpNullComparisonBehavior = ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior; + var disableFilterOverProjectionSimplificationForCustomFunctions = ObjectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions; if (cacheEntry != null) { // The cache entry has already been retrieved, so compute the effective merge option with the following precedence: @@ -77,7 +78,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg // Prepare the execution plan using the command tree and the computed effective merge option var tree = DbQueryCommandTree.FromValidExpression( - ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !useCSharpNullComparisonBehavior); + ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !useCSharpNullComparisonBehavior, disableFilterOverProjectionSimplificationForCustomFunctions); plan = _objectQueryExecutionPlanFactory.Prepare( ObjectContext, tree, ElementType, mergeOption, EffectiveStreamingBehavior, converter.PropagatedSpan, parameters, converter.AliasGenerator); @@ -110,7 +111,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg var queryExpression = converter.Convert(); var parameters = converter.GetParameters(); var tree = DbQueryCommandTree.FromValidExpression( - ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !useCSharpNullComparisonBehavior); + ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !useCSharpNullComparisonBehavior, disableFilterOverProjectionSimplificationForCustomFunctions); // If a cache entry for this compiled query's cache key was not successfully retrieved, then it must be created now. // Note that this is only possible after converting the LINQ expression and discovering the propagated merge option, diff --git a/src/EntityFramework/Core/Objects/ELinq/ELinqQueryState.cs b/src/EntityFramework/Core/Objects/ELinq/ELinqQueryState.cs index 5462682bee..9aef511b03 100644 --- a/src/EntityFramework/Core/Objects/ELinq/ELinqQueryState.cs +++ b/src/EntityFramework/Core/Objects/ELinq/ELinqQueryState.cs @@ -25,6 +25,7 @@ internal class ELinqQueryState : ObjectQueryState private Func _recompileRequired; private IEnumerable> _linqParameters; private bool _useCSharpNullComparisonBehavior; + private bool _disableFilterOverProjectionSimplificationForCustomFunctions; private readonly ObjectQueryExecutionPlanFactory _objectQueryExecutionPlanFactory; #endregion @@ -53,6 +54,7 @@ internal ELinqQueryState( _expression = expression; _useCSharpNullComparisonBehavior = context.ContextOptions.UseCSharpNullComparisonBehavior; + _disableFilterOverProjectionSimplificationForCustomFunctions = context.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions; _objectQueryExecutionPlanFactory = objectQueryExecutionPlanFactory ?? new ObjectQueryExecutionPlanFactory(); } @@ -102,7 +104,8 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg if ((explicitMergeOption.HasValue && explicitMergeOption.Value != plan.MergeOption) || _recompileRequired() - || ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior != _useCSharpNullComparisonBehavior) + || ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior != _useCSharpNullComparisonBehavior + || ObjectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions != _disableFilterOverProjectionSimplificationForCustomFunctions) { plan = null; } @@ -133,6 +136,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg converter.PropagatedMergeOption); _useCSharpNullComparisonBehavior = ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior; + _disableFilterOverProjectionSimplificationForCustomFunctions = ObjectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions; // If parameters were aggregated from referenced (non-LINQ) ObjectQuery instances then add them to the parameters collection _linqParameters = converter.GetParameters(); @@ -186,7 +190,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg if (plan == null) { var tree = DbQueryCommandTree.FromValidExpression( - ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !_useCSharpNullComparisonBehavior); + ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, !_useCSharpNullComparisonBehavior, _disableFilterOverProjectionSimplificationForCustomFunctions); plan = _objectQueryExecutionPlanFactory.Prepare( ObjectContext, tree, ElementType, mergeOption, EffectiveStreamingBehavior, converter.PropagatedSpan, null, converter.AliasGenerator); diff --git a/src/EntityFramework/Core/Objects/Internal/EntitySqlQueryState.cs b/src/EntityFramework/Core/Objects/Internal/EntitySqlQueryState.cs index bf31a4051a..bb93072508 100644 --- a/src/EntityFramework/Core/Objects/Internal/EntitySqlQueryState.cs +++ b/src/EntityFramework/Core/Objects/Internal/EntitySqlQueryState.cs @@ -186,7 +186,7 @@ internal override ObjectQueryExecutionPlan GetExecutionPlan(MergeOption? forMerg Debug.Assert(queryExpression != null, "EntitySqlQueryState.Parse returned null expression?"); var tree = DbQueryCommandTree.FromValidExpression( ObjectContext.MetadataWorkspace, DataSpace.CSpace, queryExpression, - useDatabaseNullSemantics: true); + useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false); plan = _objectQueryExecutionPlanFactory.Prepare( ObjectContext, tree, ElementType, mergeOption, EffectiveStreamingBehavior, Span, null, DbExpressionBuilder.AliasGenerator); diff --git a/src/EntityFramework/Core/Objects/Internal/ObjectQueryExecutionPlanFactory.cs b/src/EntityFramework/Core/Objects/Internal/ObjectQueryExecutionPlanFactory.cs index 034e2a8973..c10dd0ced1 100644 --- a/src/EntityFramework/Core/Objects/Internal/ObjectQueryExecutionPlanFactory.cs +++ b/src/EntityFramework/Core/Objects/Internal/ObjectQueryExecutionPlanFactory.cs @@ -35,7 +35,7 @@ public virtual ObjectQueryExecutionPlan Prepare( if (ObjectSpanRewriter.TryRewrite(tree, span, mergeOption, aliasGenerator, out spannedQuery, out spanInfo)) { tree = DbQueryCommandTree.FromValidExpression( - tree.MetadataWorkspace, tree.DataSpace, spannedQuery, tree.UseDatabaseNullSemantics); + tree.MetadataWorkspace, tree.DataSpace, spannedQuery, tree.UseDatabaseNullSemantics, tree.DisableFilterOverProjectionSimplificationForCustomFunctions); } else { diff --git a/src/EntityFramework/Core/Objects/ObjectContext.cs b/src/EntityFramework/Core/Objects/ObjectContext.cs index 82ab777285..3f0760cf47 100644 --- a/src/EntityFramework/Core/Objects/ObjectContext.cs +++ b/src/EntityFramework/Core/Objects/ObjectContext.cs @@ -2869,7 +2869,7 @@ internal virtual Tuple PrepareRefreshQuery( // Initialize the command tree used to issue the refresh query. var tree = DbQueryCommandTree.FromValidExpression( - MetadataWorkspace, DataSpace.CSpace, refreshQuery, useDatabaseNullSemantics: true); + MetadataWorkspace, DataSpace.CSpace, refreshQuery, useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false); // Evaluate the refresh query using ObjectQuery and process the results to update the ObjectStateManager. var mergeOption = (RefreshMode.StoreWins == refreshMode diff --git a/src/EntityFramework/Core/Objects/ObjectContextOptions.cs b/src/EntityFramework/Core/Objects/ObjectContextOptions.cs index 309ad8d094..4e657adc97 100644 --- a/src/EntityFramework/Core/Objects/ObjectContextOptions.cs +++ b/src/EntityFramework/Core/Objects/ObjectContextOptions.cs @@ -62,5 +62,7 @@ internal ObjectContextOptions() /// /// true if the C# NullComparison behavior should be used; otherwise, false. public bool UseCSharpNullComparisonBehavior { get; set; } + + public bool DisableFilterOverProjectionSimplificationForCustomFunctions { get; set; } } } diff --git a/src/EntityFramework/Core/Query/PlanCompiler/CTreeGenerator.cs b/src/EntityFramework/Core/Query/PlanCompiler/CTreeGenerator.cs index 1adc02f3a9..1ee8b5bb12 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/CTreeGenerator.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/CTreeGenerator.cs @@ -370,7 +370,7 @@ private CTreeGenerator(Command itree, Node toConvert) // Create the query command tree using database null semantics because this class is only // used during the CodeGen phase which occurs after the NullSemantics phase of plan compiler. _queryTree = DbQueryCommandTree.FromValidExpression( - itree.MetadataWorkspace, DataSpace.SSpace, queryExpression, useDatabaseNullSemantics: true); + itree.MetadataWorkspace, DataSpace.SSpace, queryExpression, useDatabaseNullSemantics: true, disableFilterOverProjectionSimplificationForCustomFunctions: false); } #endregion diff --git a/src/EntityFramework/Core/Query/PlanCompiler/FilterOpRules.cs b/src/EntityFramework/Core/Query/PlanCompiler/FilterOpRules.cs index 7e300d9d5d..1ff1d0a2ae 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/FilterOpRules.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/FilterOpRules.cs @@ -3,6 +3,7 @@ namespace System.Data.Entity.Core.Query.PlanCompiler { using System.Collections.Generic; + using System.Data.Entity.Core.Common; using System.Data.Entity.Core.Query.InternalTrees; using System.Diagnostics.CodeAnalysis; @@ -144,6 +145,12 @@ private static bool ProcessFilterOverProject(RuleProcessingContext context, Node return false; } + // check to see that this predicate doesn't reference user-defined functions + if (trc.IncludeCustomFunctionOp(predicateNode, varMap)) + { + return false; + } + // // Try to remap the predicate in terms of the definitions of the Vars // diff --git a/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs b/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs index 62d4c677e9..9f1f11758b 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs @@ -181,6 +181,11 @@ internal ConstraintManager ConstraintManager } } + internal bool DisableFilterOverProjectionSimplificationForCustomFunctions + { + get { return m_ctree.DisableFilterOverProjectionSimplificationForCustomFunctions; } + } + #if DEBUG /// /// Get the current plan compiler phase diff --git a/src/EntityFramework/Core/Query/PlanCompiler/TransformationRulesContext.cs b/src/EntityFramework/Core/Query/PlanCompiler/TransformationRulesContext.cs index a29fc804ab..9a5b5e52ca 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/TransformationRulesContext.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/TransformationRulesContext.cs @@ -262,6 +262,52 @@ internal bool IsScalarOpTree(Node node, Dictionary varRefMap) return IsScalarOpTree(node, varRefMap, ref nodeCount); } + /// + /// Is this tree uses user-defined functions + /// Simplifing query with UDFs could caused to suboptimal plans + /// + /// Current subtree to process + /// Mapped variables + /// + internal bool IncludeCustomFunctionOp(Node node, Dictionary varMap) + { + if (!m_compilerState.DisableFilterOverProjectionSimplificationForCustomFunctions) + { + return false; + } + + PlanCompiler.Assert(varMap != null, "Null varRef map"); + + if (node.Op.OpType == OpType.VarRef) + { + var varRefOp = (VarRefOp)node.Op; + if (varMap.TryGetValue(varRefOp.Var, out var newNode)) + { + return IncludeCustomFunctionOp(newNode, varMap); + } + } + + if (node.Op.OpType == OpType.Function) + { + var functionOp = node.Op as FunctionOp; + if (!functionOp.Function.BuiltInAttribute) + { + return true; + } + } + + // Simply process the result of the children. + for (var i = 0; i < node.Children.Count; i++) + { + if (IncludeCustomFunctionOp(node.Children[i], varMap)) + { + return true; + } + } + + return false; + } + // // Get a mapping from Var->Expression for a VarDefListOp tree. This information // will be used by later stages to replace all references to the Vars by the diff --git a/src/EntityFramework/Infrastructure/DbContextConfiguration.cs b/src/EntityFramework/Infrastructure/DbContextConfiguration.cs index 75cd54b1de..ac93e11b5c 100644 --- a/src/EntityFramework/Infrastructure/DbContextConfiguration.cs +++ b/src/EntityFramework/Infrastructure/DbContextConfiguration.cs @@ -137,6 +137,20 @@ public bool UseDatabaseNullSemantics set { _internalContext.UseDatabaseNullSemantics = value; } } + /// + /// By default expression like + /// .Select(x => NewProperty = func(x.Property)).Where(x => x.NewProperty == ...) + /// are simplified to avoid nested SELECT + /// In some cases, simplifing query with UDFs could caused to suboptimal plans due to calling UDF twice. + /// Also some SQL functions aren't allow in WHERE clause. + /// Disabling that behavior + /// + public bool DisableFilterOverProjectionSimplificationForCustomFunctions + { + get { return _internalContext.DisableFilterOverProjectionSimplificationForCustomFunctions; } + set { _internalContext.DisableFilterOverProjectionSimplificationForCustomFunctions = value; } + } + /// /// Gets or sets a value indicating whether the /// method is called automatically by methods of and related classes. diff --git a/src/EntityFramework/Internal/EagerInternalContext.cs b/src/EntityFramework/Internal/EagerInternalContext.cs index 9ffdf80580..659c1dd6c5 100644 --- a/src/EntityFramework/Internal/EagerInternalContext.cs +++ b/src/EntityFramework/Internal/EagerInternalContext.cs @@ -248,6 +248,12 @@ public override bool UseDatabaseNullSemantics set { ObjectContextInUse.ContextOptions.UseCSharpNullComparisonBehavior = !value; } } + public override bool DisableFilterOverProjectionSimplificationForCustomFunctions + { + get { return !ObjectContextInUse.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions; } + set { ObjectContextInUse.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions = !value; } + } + public override int? CommandTimeout { get { return ObjectContextInUse.CommandTimeout; } diff --git a/src/EntityFramework/Internal/InternalContext.cs b/src/EntityFramework/Internal/InternalContext.cs index 56e559e671..81fa5fbb69 100644 --- a/src/EntityFramework/Internal/InternalContext.cs +++ b/src/EntityFramework/Internal/InternalContext.cs @@ -610,6 +610,16 @@ private Action CreateInitializationAction(IDatabaseInitializer public abstract bool UseDatabaseNullSemantics { get; set; } + /// + /// By default expression like + /// .Select(x => NewProperty = func(x.Property)).Where(x => x.NewProperty == ...) + /// are simplified to avoid nested SELECT + /// In some cases, simplifing query with UDFs could caused to suboptimal plans due to calling UDF twice. + /// Also some SQL functions aren't allow in WHERE clause. + /// Disabling that behavior + /// + public abstract bool DisableFilterOverProjectionSimplificationForCustomFunctions { get; set; } + public abstract int? CommandTimeout { get; set; } // diff --git a/src/EntityFramework/Internal/LazyInternalContext.cs b/src/EntityFramework/Internal/LazyInternalContext.cs index c1cf15edb6..aeda0142a1 100644 --- a/src/EntityFramework/Internal/LazyInternalContext.cs +++ b/src/EntityFramework/Internal/LazyInternalContext.cs @@ -463,6 +463,7 @@ var model _objectContext.ContextOptions.LazyLoadingEnabled = _initialLazyLoadingFlag; _objectContext.ContextOptions.ProxyCreationEnabled = _initialProxyCreationFlag; _objectContext.ContextOptions.UseCSharpNullComparisonBehavior = !_useDatabaseNullSemanticsFlag; + _objectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions = _disableFilterOverProjectionSimplificationForCustomFunctions; _objectContext.CommandTimeout = _commandTimeout; _objectContext.ContextOptions.UseConsistentNullReferenceBehavior = true; @@ -804,6 +805,31 @@ public override bool UseDatabaseNullSemantics } } + private bool _disableFilterOverProjectionSimplificationForCustomFunctions; + + public override bool DisableFilterOverProjectionSimplificationForCustomFunctions + { + get + { + var objectContext = ObjectContextInUse; + return objectContext != null + ? !objectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions + : _disableFilterOverProjectionSimplificationForCustomFunctions; + } + set + { + var objectContext = ObjectContextInUse; + if (objectContext != null) + { + objectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions = !value; + } + else + { + _disableFilterOverProjectionSimplificationForCustomFunctions = value; + } + } + } + public override int? CommandTimeout { get diff --git a/src/EntityFramework/Internal/MockingProxies/ObjectContextProxy.cs b/src/EntityFramework/Internal/MockingProxies/ObjectContextProxy.cs index 42306297f0..a6f9a3ecb0 100644 --- a/src/EntityFramework/Internal/MockingProxies/ObjectContextProxy.cs +++ b/src/EntityFramework/Internal/MockingProxies/ObjectContextProxy.cs @@ -91,6 +91,7 @@ public virtual void CopyContextOptions(ObjectContextProxy source) _objectContext.ContextOptions.UseLegacyPreserveChangesBehavior = source._objectContext.ContextOptions.UseLegacyPreserveChangesBehavior; _objectContext.CommandTimeout = source._objectContext.CommandTimeout; + _objectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions = source._objectContext.ContextOptions.DisableFilterOverProjectionSimplificationForCustomFunctions; _objectContext.InterceptionContext = source._objectContext.InterceptionContext.WithObjectContext(_objectContext); } diff --git a/test/EntityFramework/FunctionalTests/Query/FilterOpRulesTests.cs b/test/EntityFramework/FunctionalTests/Query/FilterOpRulesTests.cs index 2edad2a823..a2645d8d1a 100644 --- a/test/EntityFramework/FunctionalTests/Query/FilterOpRulesTests.cs +++ b/test/EntityFramework/FunctionalTests/Query/FilterOpRulesTests.cs @@ -2,6 +2,10 @@ namespace System.Data.Entity.Query { + using System.Data.Entity.Core.Common; + using System.Data.Entity.Core.Metadata.Edm; + using System.Data.Entity.Infrastructure; + using System.Data.Entity.ModelConfiguration.Conventions; using System.Linq; using Xunit; @@ -30,6 +34,47 @@ static BlogContext() { Database.SetInitializer(null); } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Conventions.Add(new CustomFunction()); + } + } + + public static class CustomFunctions + { + [DbFunction("SqlServer", "MyCustomFunc")] + public static int MyCustomFunc(string value) + { + throw new NotSupportedException("Direct calls are not supported."); + } + } + + public class CustomFunction : IConvention, IStoreModelConvention + { + public void Apply(EntityContainer item, DbModel model) + { + var customFuncStore = EdmFunction.Create("MyCustomFunc", "SqlServer", DataSpace.SSpace, new EdmFunctionPayload + { + ParameterTypeSemantics = ParameterTypeSemantics.AllowImplicitConversion, + IsComposable = true, + IsAggregate = false, + StoreFunctionName = "MyCustomFunc", + IsBuiltIn = false, + ReturnParameters = new[] + { + FunctionParameter.Create("ReturnType", PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Int32), ParameterMode.ReturnValue) + }, + Parameters = new[] + { + FunctionParameter.Create("input", PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.String), ParameterMode.In), + } + }, null); + + + model.StoreModel.AddItem(customFuncStore); + } } [Fact] @@ -120,9 +165,9 @@ FROM [dbo].[BlogEntries] AS [Extent2] context.Configuration.UseDatabaseNullSemantics = false; var query = from b in context.Blogs - from e in context.BlogEntries.Where(e => e.Name == b.Name).Take(1).DefaultIfEmpty() - where b.Name == e.Name - select b; + from e in context.BlogEntries.Where(e => e.Name == b.Name).Take(1).DefaultIfEmpty() + where b.Name == e.Name + select b; QueryTestHelpers.VerifyDbQuery(query, expectedSql); } @@ -179,5 +224,98 @@ from e in context.BlogEntries.Where(e => e.Name == b.Name).Take(1).DefaultIfEmpt QueryTestHelpers.VerifyDbQuery(query, expectedSql); } } + + [Fact] + public void Rule_FilterOverProject_promotes_to_single_Select_if_builtint_function() + { + var expectedSql = +@"SELECT + [Extent1].[Id] AS [Id], + CAST(LEN([Extent1].[Name]) AS int) AS [C1] + FROM [dbo].[Blogs] AS [Extent1] + WHERE (CAST(LEN([Extent1].[Name]) AS int)) > 10"; + + using (var context = new BlogContext()) + { + context.Configuration.UseDatabaseNullSemantics = true; + + var query = context.Blogs.Select(b => new { b.Id, Len = b.Name.Length }).Where(b => b.Len > 10); + + QueryTestHelpers.VerifyDbQuery(query, expectedSql); + } + } + + [Fact] + public void Rule_FilterOverProject_does_not_promote_to_single_Select_if_custom_function_and_does_opt_in() + { + var expectedSql = +@"SELECT + [Project1].[Id] AS [Id], + [Project1].[C1] AS [C1] + FROM ( SELECT + [Extent1].[Id] AS [Id], + [SqlServer].[MyCustomFunc]([Extent1].[Name]) AS [C1] + FROM [dbo].[Blogs] AS [Extent1] + ) AS [Project1] + WHERE ([Project1].[Id] > 10) AND ([Project1].[C1] > 10)"; + + using (var context = new BlogContext()) + { + context.Configuration.UseDatabaseNullSemantics = true; + context.Configuration.DisableFilterOverProjectionSimplificationForCustomFunctions = true; + + var query = context.Blogs.Select(b => new { b.Id, Len = CustomFunctions.MyCustomFunc(b.Name) }).Where(b => b.Id > 10 && b.Len > 10); + QueryTestHelpers.VerifyDbQuery(query, expectedSql); + } + } + + [Fact] + public void Rule_FilterOverProject_does_not_promote_to_single_Select_with_limit_if_custom_function_and_does_opt_in() + { + var expectedSql = +@"SELECT TOP (10) + [Project1].[Id] AS [Id], + [Project1].[C1] AS [C1] + FROM ( SELECT + [Extent1].[Id] AS [Id], + [SqlServer].[MyCustomFunc]([Extent1].[Name]) AS [C1] + FROM [dbo].[Blogs] AS [Extent1] + ) AS [Project1] + WHERE ([Project1].[Id] > 10) AND ([Project1].[C1] > 10) + ORDER BY [Project1].[Id] ASC"; + + using (var context = new BlogContext()) + { + context.Configuration.UseDatabaseNullSemantics = true; + context.Configuration.DisableFilterOverProjectionSimplificationForCustomFunctions = true; + + var query = context.Blogs.Select(b => new { b.Id, Len = CustomFunctions.MyCustomFunc(b.Name) }).Where(b => b.Id > 10 && b.Len > 10).OrderBy(b => b.Id).Take(10); + QueryTestHelpers.VerifyDbQuery(query, expectedSql); + } + } + + + [Fact] + public void Rule_FilterOverProject_does_promote_to_single_Select_if_custom_function_and_doesnt_opt_in() + { + var expectedSql = +@"SELECT + [Extent1].[Id] AS [Id], + [SqlServer].[MyCustomFunc]([Extent1].[Name]) AS [C1] + FROM [dbo].[Blogs] AS [Extent1] + WHERE ([SqlServer].[MyCustomFunc]([Extent1].[Name])) > 10"; + + using (var context = new BlogContext()) + { + context.Configuration.UseDatabaseNullSemantics = true; + context.Configuration.DisableFilterOverProjectionSimplificationForCustomFunctions = false; // false is default, but using explicit valueto make it obvious + + var query = context.Blogs.Select(b => new { b.Id, Len = CustomFunctions.MyCustomFunc(b.Name) }).Where(b => b.Len > 10); + QueryTestHelpers.VerifyDbQuery(query, expectedSql); + } + } + + + } }