//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- namespace System.Data.Common.EntitySql { using System; using System.Collections.Generic; using System.Data.Common.CommandTrees; using System.Data.Common.CommandTrees.ExpressionBuilder; using System.Data.Entity; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Globalization; using System.Linq; /// /// Represents eSQL expression class. /// internal enum ExpressionResolutionClass { /// /// A value expression such as a literal, variable or a value-returning expression. /// Value, /// /// An expression returning an entity container. /// EntityContainer, /// /// An expression returning a metadata member such as a type, function group or namespace. /// MetadataMember } /// /// Abstract class representing the result of an eSQL expression classification. /// internal abstract class ExpressionResolution { protected ExpressionResolution(ExpressionResolutionClass @class) { ExpressionClass = @class; } internal readonly ExpressionResolutionClass ExpressionClass; internal abstract string ExpressionClassName { get; } } /// /// Represents an eSQL expression classified as . /// internal sealed class ValueExpression : ExpressionResolution { internal ValueExpression(DbExpression value) : base(ExpressionResolutionClass.Value) { Value = value; } internal override string ExpressionClassName { get { return ValueClassName; } } internal static string ValueClassName { get { return Strings.LocalizedValueExpression; } } /// /// Null if represents the untyped null. /// internal readonly DbExpression Value; } /// /// Represents an eSQL expression classified as . /// internal sealed class EntityContainerExpression : ExpressionResolution { internal EntityContainerExpression(EntityContainer entityContainer) : base(ExpressionResolutionClass.EntityContainer) { EntityContainer = entityContainer; } internal override string ExpressionClassName { get { return EntityContainerClassName; } } internal static string EntityContainerClassName { get { return Strings.LocalizedEntityContainerExpression; } } internal readonly EntityContainer EntityContainer; } /// /// Implements the semantic resolver in the context of a metadata workspace and typespace. /// /// not thread safe internal sealed class SemanticResolver { #region Fields private readonly ParserOptions _parserOptions; private readonly Dictionary _parameters; private readonly Dictionary _variables; private readonly TypeResolver _typeResolver; private readonly ScopeManager _scopeManager; private readonly List _scopeRegions = new List(); private bool _ignoreEntityContainerNameResolution = false; private GroupAggregateInfo _currentGroupAggregateInfo = null; private uint _namegenCounter = 0; #endregion #region Constructors /// /// Creates new instance of . /// internal static SemanticResolver Create(Perspective perspective, ParserOptions parserOptions, IEnumerable parameters, IEnumerable variables) { EntityUtil.CheckArgumentNull(perspective, "perspective"); EntityUtil.CheckArgumentNull(parserOptions, "parserOptions"); return new SemanticResolver( parserOptions, ProcessParameters(parameters, parserOptions), ProcessVariables(variables, parserOptions), new TypeResolver(perspective, parserOptions)); } /// /// Creates a copy of with clean scopes and shared inline function definitions inside of the type resolver. /// internal SemanticResolver CloneForInlineFunctionConversion() { return new SemanticResolver( _parserOptions, _parameters, _variables, _typeResolver); } private SemanticResolver(ParserOptions parserOptions, Dictionary parameters, Dictionary variables, TypeResolver typeResolver) { _parserOptions = parserOptions; _parameters = parameters; _variables = variables; _typeResolver = typeResolver; // // Creates Scope manager // _scopeManager = new ScopeManager(this.NameComparer); // // Push a root scope region // EnterScopeRegion(); // // Add command free variables to the root scope // foreach (DbVariableReferenceExpression variable in _variables.Values) { this.CurrentScope.Add(variable.VariableName, new FreeVariableScopeEntry(variable)); } } /// /// Validates that the specified parameters have valid, non-duplicated names /// /// The set of query parameters /// A valid dictionary that maps parameter names to s using the current NameComparer private static Dictionary ProcessParameters(IEnumerable paramDefs, ParserOptions parserOptions) { Dictionary retParams = new Dictionary(parserOptions.NameComparer); if (paramDefs != null) { foreach (DbParameterReferenceExpression paramDef in paramDefs) { if (retParams.ContainsKey(paramDef.ParameterName)) { throw EntityUtil.EntitySqlError(Strings.MultipleDefinitionsOfParameter(paramDef.ParameterName)); } Debug.Assert(paramDef.ResultType.IsReadOnly, "paramDef.ResultType.IsReadOnly must be set"); retParams.Add(paramDef.ParameterName, paramDef); } } return retParams; } /// /// Validates that the specified variables have valid, non-duplicated names /// /// The set of free variables /// A valid dictionary that maps variable names to s using the current NameComparer private static Dictionary ProcessVariables(IEnumerable varDefs, ParserOptions parserOptions) { Dictionary retVars = new Dictionary(parserOptions.NameComparer); if (varDefs != null) { foreach (DbVariableReferenceExpression varDef in varDefs) { if (retVars.ContainsKey(varDef.VariableName)) { throw EntityUtil.EntitySqlError(Strings.MultipleDefinitionsOfVariable(varDef.VariableName)); } Debug.Assert(varDef.ResultType.IsReadOnly, "varDef.ResultType.IsReadOnly must be set"); retVars.Add(varDef.VariableName, varDef); } } return retVars; } #endregion #region Properties /// /// Returns ordinary command parameters. Empty dictionary in case of no parameters. /// internal Dictionary Parameters { get { return _parameters; } } /// /// Returns command free variables. Empty dictionary in case of no variables. /// internal Dictionary Variables { get { return _variables; } } /// /// TypeSpace/Metadata/Perspective dependent type resolver. /// internal TypeResolver TypeResolver { get { return _typeResolver; } } /// /// Returns current Parser Options. /// internal ParserOptions ParserOptions { get { return _parserOptions; } } /// /// Returns the current string comparer. /// internal StringComparer NameComparer { get { return _parserOptions.NameComparer; } } /// /// Returns the list of scope regions: outer followed by inner. /// internal IEnumerable ScopeRegions { get { return _scopeRegions; } } /// /// Returns the current scope region. /// internal ScopeRegion CurrentScopeRegion { get { return _scopeRegions[_scopeRegions.Count - 1]; } } /// /// Returns the current scope. /// internal Scope CurrentScope { get { return _scopeManager.CurrentScope; } } /// /// Returns index of the current scope. /// internal int CurrentScopeIndex { get { return _scopeManager.CurrentScopeIndex; } } /// /// Returns the current group aggregate info when processing group aggregate argument. /// internal GroupAggregateInfo CurrentGroupAggregateInfo { get { return _currentGroupAggregateInfo; } } #endregion #region GetExpressionFromScopeEntry /// /// Returns the appropriate expression from a given scope entry. /// May return null for scope entries like . /// private DbExpression GetExpressionFromScopeEntry(ScopeEntry scopeEntry, int scopeIndex, string varName, ErrorContext errCtx) { // // If // 1) we are in the context of a group aggregate or group key, // 2) and the scopeEntry can have multiple interpretations depending on the aggregation context, // 3) and the defining scope region of the scopeEntry is outer or equal to the defining scope region of the group aggregate, // 4) and the defining scope region of the scopeEntry is not performing conversion of a group key definition, // Then the expression that corresponds to the scopeEntry is either the GroupVarBasedExpression or the GroupAggBasedExpression. // Otherwise the default expression that corresponds to the scopeEntry is provided by scopeEntry.GetExpression(...) call. // // Explanation for #2 from the list above: // A scope entry may have multiple aggregation-context interpretations: // - An expression in the context of a group key definition, obtained by scopeEntry.GetExpression(...); // Example: select k1 from {0} as a group by a%2 as k1 // ^^^ // - An expression in the context of a function aggregate, provided by iGroupExpressionExtendedInfo.GroupVarBasedExpression; // Example: select max( a ) from {0} as a group by a%2 as k1 // ^^^ // - An expression in the context of a group partition, provided by iGroupExpressionExtendedInfo.GroupAggBasedExpression; // Example: select GroupPartition( a ) from {0} as a group by a%2 as k1 // ^^^ // Note that expressions obtained from aggregation-context-dependent scope entries outside of the three contexts mentioned above // will default to the value returned by the scopeEntry.GetExpression(...) call. This value is the same as in the group key definition context. // These expressions have correct result types which enables partial expression validation. // However the contents of the expressions are invalid outside of the group key definitions, hence they can not appear in the final expression tree. // SemanticAnalyzer.ProcessGroupByClause(...) method guarantees that such expressions are only temporarily used during GROUP BY clause processing and // dropped afterwards. // Example: select a, k1 from {0} as a group by a%2 as k1 // ^^^^^ - these expressions are processed twice: once during GROUP BY and then SELECT clause processing, // the expressions obtained during GROUP BY clause processing are dropped and only // the ones obtained during SELECT clause processing are accepted. // // Explanation for #3 from the list above: // - An outer scope entry referenced inside of an aggregate may lift the aggregate to the outer scope region for evaluation, // hence such a scope entry must be interpreted in the aggregation context. See explanation for #4 below for more info. // Example: // // select // (select max(x) from {1} as y) // from {0} as x // // - If a scope entry is defined inside of a group aggregate, then the scope entry is not affected by the aggregate, // hence such a scope entry is not interpreted in the aggregation context. // Example: // // select max( // anyelement( select b from {1} as b ) // ) // from {0} as a group by a %2 as a1 // // In this query the aggregate argument contains a nested query expression. // The nested query references b. Because b is defined inside of the aggregate it is not interpreted in the aggregation context and // the expression for b should not be GroupVar/GroupAgg based, even though the reference to b appears inside of an aggregate. // // Explanation for #4 from the list above: // An aggregate evaluating on a particular scope region defines the interpretation of scope entries defined on that scope region. // In the case when an inner aggregate references a scope entry belonging to the evaluating region of an outer aggregate, the interpretation // of the scope entry is controlled by the outer aggregate, otherwise it is controlled by the inner aggregate. // Example: // // select a1 // from {0} as a group by // anyelement(select value max(a + b) from {1} as b) // as a1 // // In this query the aggregate inside of a1 group key definition, the max(a + b), references scope entry a. // Because a is referenced inside of the group key definition (which serves as an outer aggregate) and the key definition belongs to // the same scope region as a, a is interpreted in the context of the group key definition, not the function aggregate and // the expression for a is obtained by scopeEntry.GetExpression(...) call, not iGroupExpressionExtendedInfo.GroupVarBasedExpression. // DbExpression expr = scopeEntry.GetExpression(varName, errCtx); Debug.Assert(expr != null, "scopeEntry.GetExpression(...) returned null"); if (_currentGroupAggregateInfo != null) { // // Make sure defining scope regions agree as described above. // Outer scope region has smaller index value than the inner. // ScopeRegion definingScopeRegionOfScopeEntry = GetDefiningScopeRegion(scopeIndex); if (definingScopeRegionOfScopeEntry.ScopeRegionIndex <= _currentGroupAggregateInfo.DefiningScopeRegion.ScopeRegionIndex) { // // Let the group aggregate know the scope of the scope entry it references. // This affects the scope region that will evaluate the group aggregate. // _currentGroupAggregateInfo.UpdateScopeIndex(scopeIndex, this); IGroupExpressionExtendedInfo iGroupExpressionExtendedInfo = scopeEntry as IGroupExpressionExtendedInfo; if (iGroupExpressionExtendedInfo != null) { // // Find the aggregate that controls interpretation of the current scope entry. // This would be a containing aggregate with the defining scope region matching definingScopeRegionOfScopeEntry. // If there is no such aggregate, then the current containing aggregate controls interpretation. // GroupAggregateInfo expressionInterpretationContext; for (expressionInterpretationContext = _currentGroupAggregateInfo; expressionInterpretationContext != null && expressionInterpretationContext.DefiningScopeRegion.ScopeRegionIndex >= definingScopeRegionOfScopeEntry.ScopeRegionIndex; expressionInterpretationContext = expressionInterpretationContext.ContainingAggregate) { if (expressionInterpretationContext.DefiningScopeRegion.ScopeRegionIndex == definingScopeRegionOfScopeEntry.ScopeRegionIndex) { break; } } if (expressionInterpretationContext == null || expressionInterpretationContext.DefiningScopeRegion.ScopeRegionIndex < definingScopeRegionOfScopeEntry.ScopeRegionIndex) { expressionInterpretationContext = _currentGroupAggregateInfo; } switch (expressionInterpretationContext.AggregateKind) { case GroupAggregateKind.Function: if (iGroupExpressionExtendedInfo.GroupVarBasedExpression != null) { expr = iGroupExpressionExtendedInfo.GroupVarBasedExpression; } break; case GroupAggregateKind.Partition: if (iGroupExpressionExtendedInfo.GroupAggBasedExpression != null) { expr = iGroupExpressionExtendedInfo.GroupAggBasedExpression; } break; case GroupAggregateKind.GroupKey: // // User the current expression obtained from scopeEntry.GetExpression(...) // break; default: Debug.Fail("Unexpected group aggregate kind."); break; } } } } return expr; } #endregion #region Name resolution #region Resolve simple / metadata member name internal IDisposable EnterIgnoreEntityContainerNameResolution() { Debug.Assert(!_ignoreEntityContainerNameResolution, "EnterIgnoreEntityContainerNameResolution() is not reentrant."); _ignoreEntityContainerNameResolution = true; return new Disposer(delegate { Debug.Assert(this._ignoreEntityContainerNameResolution, "_ignoreEntityContainerNameResolution must be true."); this._ignoreEntityContainerNameResolution = false; }); } internal ExpressionResolution ResolveSimpleName(string name, bool leftHandSideOfMemberAccess, ErrorContext errCtx) { Debug.Assert(!String.IsNullOrEmpty(name), "name must not be null or empty"); // // Try resolving as a scope entry. // ScopeEntry scopeEntry; int scopeIndex; if (TryScopeLookup(name, out scopeEntry, out scopeIndex)) { // // Check for invalid join left expression correlation. // if (scopeEntry.EntryKind == ScopeEntryKind.SourceVar && ((SourceScopeEntry)scopeEntry).IsJoinClauseLeftExpr) { throw EntityUtil.EntitySqlError(errCtx, Strings.InvalidJoinLeftCorrelation); } // // Set correlation flag. // SetScopeRegionCorrelationFlag(scopeIndex); return new ValueExpression(GetExpressionFromScopeEntry(scopeEntry, scopeIndex, name, errCtx)); } // // Try resolving as a member of the default entity container. // EntityContainer defaultEntityContainer = this.TypeResolver.Perspective.GetDefaultContainer(); ExpressionResolution defaultEntityContainerResolution; if (defaultEntityContainer != null && TryResolveEntityContainerMemberAccess(defaultEntityContainer, name, errCtx, out defaultEntityContainerResolution)) { return defaultEntityContainerResolution; } if (!_ignoreEntityContainerNameResolution) { // // Try resolving as an entity container. // EntityContainer entityContainer; if (this.TypeResolver.Perspective.TryGetEntityContainer(name, _parserOptions.NameComparisonCaseInsensitive /*ignoreCase*/, out entityContainer)) { return new EntityContainerExpression(entityContainer); } } // // Otherwise, resolve as an unqualified name. // return this.TypeResolver.ResolveUnqualifiedName(name, leftHandSideOfMemberAccess /* partOfQualifiedName */, errCtx); } internal MetadataMember ResolveSimpleFunctionName(string name, ErrorContext errCtx) { // // "Foo()" represents a simple function name. Resolve it as an unqualified name by calling the type resolver directly. // Note that calling type resolver directly will avoid resolution of the identifier as a local variable or entity container // (these resolutions are performed only by ResolveSimpleName(...)). // var resolution = this.TypeResolver.ResolveUnqualifiedName(name, false /* partOfQualifiedName */, errCtx); if (resolution.MetadataMemberClass == MetadataMemberClass.Namespace) { // // Try resolving as a function import inside the default entity container. // EntityContainer defaultEntityContainer = this.TypeResolver.Perspective.GetDefaultContainer(); ExpressionResolution defaultEntityContainerResolution; if (defaultEntityContainer != null && TryResolveEntityContainerMemberAccess(defaultEntityContainer, name, errCtx, out defaultEntityContainerResolution) && defaultEntityContainerResolution.ExpressionClass == ExpressionResolutionClass.MetadataMember) { resolution = (MetadataMember)defaultEntityContainerResolution; } } return resolution; } /// /// Performs scope lookup returning the scope entry and its index. /// private bool TryScopeLookup(string key, out ScopeEntry scopeEntry, out int scopeIndex) { scopeEntry = null; scopeIndex = -1; for (int i = CurrentScopeIndex; i >= 0; i--) { if (_scopeManager.GetScopeByIndex(i).TryLookup(key, out scopeEntry)) { scopeIndex = i; return true; } } return false; } internal MetadataMember ResolveMetadataMemberName(string[] name, ErrorContext errCtx) { return this.TypeResolver.ResolveMetadataMemberName(name, errCtx); } #endregion #region Resolve member name in member access #region Resolve property access /// /// Resolve property off the . /// internal ValueExpression ResolvePropertyAccess(DbExpression valueExpr, string name, ErrorContext errCtx) { DbExpression propertyExpr; if (TryResolveAsPropertyAccess(valueExpr, name, errCtx, out propertyExpr)) { return new ValueExpression(propertyExpr); } if (TryResolveAsRefPropertyAccess(valueExpr, name, errCtx, out propertyExpr)) { return new ValueExpression(propertyExpr); } if (TypeSemantics.IsCollectionType(valueExpr.ResultType)) { throw EntityUtil.EntitySqlError(errCtx, Strings.NotAMemberOfCollection(name, valueExpr.ResultType.EdmType.FullName)); } else { throw EntityUtil.EntitySqlError(errCtx, Strings.NotAMemberOfType(name, valueExpr.ResultType.EdmType.FullName)); } } /// /// Try resolving as a property of the value returned by the . /// private bool TryResolveAsPropertyAccess(DbExpression valueExpr, string name, ErrorContext errCtx, out DbExpression propertyExpr) { Debug.Assert(valueExpr != null, "valueExpr != null"); propertyExpr = null; if (Helper.IsStructuralType(valueExpr.ResultType.EdmType)) { EdmMember member; if (TypeResolver.Perspective.TryGetMember((StructuralType)valueExpr.ResultType.EdmType, name, _parserOptions.NameComparisonCaseInsensitive /*ignoreCase*/, out member)) { Debug.Assert(member != null, "member != null"); Debug.Assert(this.NameComparer.Equals(name, member.Name), "this.NameComparer.Equals(name, member.Name)"); propertyExpr = DbExpressionBuilder.CreatePropertyExpressionFromMember(valueExpr, member); return true; } } return false; } /// /// If returns a reference, then deref and try resolving as a property of the dereferenced value. /// private bool TryResolveAsRefPropertyAccess(DbExpression valueExpr, string name, ErrorContext errCtx, out DbExpression propertyExpr) { Debug.Assert(valueExpr != null, "valueExpr != null"); propertyExpr = null; if (TypeSemantics.IsReferenceType(valueExpr.ResultType)) { DbExpression derefExpr = valueExpr.Deref(); TypeUsage derefExprType = derefExpr.ResultType; if (TryResolveAsPropertyAccess(derefExpr, name, errCtx, out propertyExpr)) { return true; } else { throw EntityUtil.EntitySqlError(errCtx, Strings.InvalidDeRefProperty(name, derefExprType.EdmType.FullName, valueExpr.ResultType.EdmType.FullName)); } } return false; } #endregion #region Resolve entity container member access /// /// Resolve entity set or function import in the /// internal ExpressionResolution ResolveEntityContainerMemberAccess(EntityContainer entityContainer, string name, ErrorContext errCtx) { ExpressionResolution resolution; if (TryResolveEntityContainerMemberAccess(entityContainer, name, errCtx, out resolution)) { return resolution; } else { throw EntityUtil.EntitySqlError(errCtx, Strings.MemberDoesNotBelongToEntityContainer(name, entityContainer.Name)); } } private bool TryResolveEntityContainerMemberAccess(EntityContainer entityContainer, string name, ErrorContext errCtx, out ExpressionResolution resolution) { EntitySetBase entitySetBase; EdmFunction functionImport; if (this.TypeResolver.Perspective.TryGetExtent(entityContainer, name, _parserOptions.NameComparisonCaseInsensitive /*ignoreCase*/, out entitySetBase)) { resolution = new ValueExpression(entitySetBase.Scan()); return true; } else if (this.TypeResolver.Perspective.TryGetFunctionImport(entityContainer, name, _parserOptions.NameComparisonCaseInsensitive /*ignoreCase*/, out functionImport)) { resolution = new MetadataFunctionGroup(functionImport.FullName, new EdmFunction[] { functionImport }); return true; } else { resolution = null; return false; } } #endregion #region Resolve metadata member access /// /// Resolve namespace, type or function in the /// internal MetadataMember ResolveMetadataMemberAccess(MetadataMember metadataMember, string name, ErrorContext errCtx) { return this.TypeResolver.ResolveMetadataMemberAccess(metadataMember, name, errCtx); } #endregion #endregion #region Resolve internal aggregate name / alternative group key name /// /// Try resolving an internal aggregate name. /// internal bool TryResolveInternalAggregateName(string name, ErrorContext errCtx, out DbExpression dbExpression) { ScopeEntry scopeEntry; int scopeIndex; if (TryScopeLookup(name, out scopeEntry, out scopeIndex)) { // // Set the correlation flag. // SetScopeRegionCorrelationFlag(scopeIndex); dbExpression = scopeEntry.GetExpression(name, errCtx); return true; } else { dbExpression = null; return false; } } /// /// Try resolving multipart identifier as an alternative name of a group key (see SemanticAnalyzer.ProcessGroupByClause(...) for more info). /// internal bool TryResolveDotExprAsGroupKeyAlternativeName(AST.DotExpr dotExpr, out ValueExpression groupKeyResolution) { groupKeyResolution = null; string[] names; ScopeEntry scopeEntry; int scopeIndex; if (IsInAnyGroupScope() && dotExpr.IsMultipartIdentifier(out names) && TryScopeLookup(TypeResolver.GetFullName(names), out scopeEntry, out scopeIndex)) { IGetAlternativeName iGetAlternativeName = scopeEntry as IGetAlternativeName; // // Accept only if names[] match alternative name part by part. // if (iGetAlternativeName != null && iGetAlternativeName.AlternativeName != null && names.SequenceEqual(iGetAlternativeName.AlternativeName, this.NameComparer)) { // // Set correlation flag // SetScopeRegionCorrelationFlag(scopeIndex); groupKeyResolution = new ValueExpression(GetExpressionFromScopeEntry(scopeEntry, scopeIndex, TypeResolver.GetFullName(names), dotExpr.ErrCtx)); return true; } } return false; } #endregion #endregion #region Name generation utils (GenerateInternalName, CreateNewAlias, InferAliasName) /// /// Generates unique internal name. /// internal string GenerateInternalName(string hint) { // string concat is much faster than String.Format return "_##" + hint + unchecked(_namegenCounter++).ToString(CultureInfo.InvariantCulture); } /// /// Creates a new alias name based on the information. /// private string CreateNewAlias(DbExpression expr) { DbScanExpression extent = expr as DbScanExpression; if (null != extent) { return extent.Target.Name; } DbPropertyExpression property = expr as DbPropertyExpression; if (null != property) { return property.Property.Name; } DbVariableReferenceExpression varRef = expr as DbVariableReferenceExpression; if (null != varRef) { return varRef.VariableName; } return GenerateInternalName(String.Empty); } /// /// Returns alias name from ast node if it contains an alias, /// otherwise creates a new alias name based on the .Expr or information. /// internal string InferAliasName(AST.AliasedExpr aliasedExpr, DbExpression convertedExpression) { if (aliasedExpr.Alias != null) { return aliasedExpr.Alias.Name; } AST.Identifier id = aliasedExpr.Expr as AST.Identifier; if (null != id) { return id.Name; } AST.DotExpr dotExpr = aliasedExpr.Expr as AST.DotExpr; string[] names; if (null != dotExpr && dotExpr.IsMultipartIdentifier(out names)) { return names[names.Length - 1]; } return CreateNewAlias(convertedExpression); } #endregion #region Scope/ScopeRegion utils /// /// Enters a new scope region. /// internal IDisposable EnterScopeRegion() { // // Push new scope (the first scope in the new scope region) // _scopeManager.EnterScope(); // // Create new scope region and push it // ScopeRegion scopeRegion = new ScopeRegion(_scopeManager, CurrentScopeIndex, _scopeRegions.Count); _scopeRegions.Add(scopeRegion); // // Return scope region disposer that rolls back the scope. // return new Disposer(delegate { Debug.Assert(this.CurrentScopeRegion == scopeRegion, "Scope region stack is corrupted."); // // Root scope region is permanent. // Debug.Assert(this._scopeRegions.Count > 1, "_scopeRegionFlags.Count > 1"); // // Reset aggregate info of AST nodes of aggregates resolved to the CurrentScopeRegion. // this.CurrentScopeRegion.GroupAggregateInfos.ForEach(groupAggregateInfo => groupAggregateInfo.DetachFromAstNode()); // // Rollback scopes of the region. // this.CurrentScopeRegion.RollbackAllScopes(); // // Remove the scope region. // this._scopeRegions.Remove(CurrentScopeRegion); }); } /// /// Rollback all scopes above the . /// internal void RollbackToScope(int scopeIndex) { _scopeManager.RollbackToScope(scopeIndex); } /// /// Enter a new scope. /// internal void EnterScope() { _scopeManager.EnterScope(); } /// /// Leave the current scope. /// internal void LeaveScope() { _scopeManager.LeaveScope(); } /// /// Returns true if any of the ScopeRegions from the closest to the outermost has IsAggregating = true /// internal bool IsInAnyGroupScope() { for (int i = 0; i < _scopeRegions.Count; i++) { if (_scopeRegions[i].IsAggregating) { return true; } } return false; } internal ScopeRegion GetDefiningScopeRegion(int scopeIndex) { // // Starting from the innermost, find the outermost scope region that contains the scope. // for (int i = _scopeRegions.Count - 1; i >= 0; --i) { if (_scopeRegions[i].ContainsScope(scopeIndex)) { return _scopeRegions[i]; } } Debug.Fail("Failed to find the defining scope region for the given scope."); return null; } /// /// Sets the scope region correlation flag based on the scope index of the referenced scope entry. /// private void SetScopeRegionCorrelationFlag(int scopeIndex) { GetDefiningScopeRegion(scopeIndex).WasResolutionCorrelated = true; } #endregion #region Group aggregate utils /// /// Enters processing of a function group aggregate. /// internal IDisposable EnterFunctionAggregate(AST.MethodExpr methodExpr, ErrorContext errCtx, out FunctionAggregateInfo aggregateInfo) { aggregateInfo = new FunctionAggregateInfo(methodExpr, errCtx, _currentGroupAggregateInfo, CurrentScopeRegion); return EnterGroupAggregate(aggregateInfo); } /// /// Enters processing of a group partition aggregate. /// internal IDisposable EnterGroupPartition(AST.GroupPartitionExpr groupPartitionExpr, ErrorContext errCtx, out GroupPartitionInfo aggregateInfo) { aggregateInfo = new GroupPartitionInfo(groupPartitionExpr, errCtx, _currentGroupAggregateInfo, CurrentScopeRegion); return EnterGroupAggregate(aggregateInfo); } /// /// Enters processing of a group partition aggregate. /// internal IDisposable EnterGroupKeyDefinition(GroupAggregateKind aggregateKind, ErrorContext errCtx, out GroupKeyAggregateInfo aggregateInfo) { aggregateInfo = new GroupKeyAggregateInfo(aggregateKind, errCtx, _currentGroupAggregateInfo, CurrentScopeRegion); return EnterGroupAggregate(aggregateInfo); } private IDisposable EnterGroupAggregate(GroupAggregateInfo aggregateInfo) { _currentGroupAggregateInfo = aggregateInfo; return new Disposer(delegate { // // First, pop the element from the stack to keep the stack valid... // Debug.Assert(this._currentGroupAggregateInfo == aggregateInfo, "Aggregare info stack is corrupted."); this._currentGroupAggregateInfo = aggregateInfo.ContainingAggregate; // // ...then validate and seal the aggregate info. // Note that this operation may throw an EntitySqlException. // aggregateInfo.ValidateAndComputeEvaluatingScopeRegion(this); }); } #endregion #region Function overload resolution (untyped null aware) internal static EdmFunction ResolveFunctionOverloads(IList functionsMetadata, IList argTypes, bool isGroupAggregateFunction, out bool isAmbiguous) { return FunctionOverloadResolver.ResolveFunctionOverloads( functionsMetadata, argTypes, UntypedNullAwareFlattenArgumentType, UntypedNullAwareFlattenParameterType, UntypedNullAwareIsPromotableTo, UntypedNullAwareIsStructurallyEqual, isGroupAggregateFunction, out isAmbiguous); } internal static TFunctionMetadata ResolveFunctionOverloads( IList functionsMetadata, IList argTypes, Func> getSignatureParams, Func getParameterTypeUsage, Func getParameterMode, bool isGroupAggregateFunction, out bool isAmbiguous) where TFunctionMetadata : class { return FunctionOverloadResolver.ResolveFunctionOverloads( functionsMetadata, argTypes, getSignatureParams, getParameterTypeUsage, getParameterMode, UntypedNullAwareFlattenArgumentType, UntypedNullAwareFlattenParameterType, UntypedNullAwareIsPromotableTo, UntypedNullAwareIsStructurallyEqual, isGroupAggregateFunction, out isAmbiguous); } private static IEnumerable UntypedNullAwareFlattenArgumentType(TypeUsage argType) { return argType != null ? TypeSemantics.FlattenType(argType) : new TypeUsage[] { null }; } private static IEnumerable UntypedNullAwareFlattenParameterType(TypeUsage paramType, TypeUsage argType) { return argType != null ? TypeSemantics.FlattenType(paramType) : new TypeUsage[] { paramType }; } private static bool UntypedNullAwareIsPromotableTo(TypeUsage fromType, TypeUsage toType) { if (fromType == null) { // // We can implicitly promote null to any type except collection. // return !Helper.IsCollectionType(toType.EdmType); } else { return TypeSemantics.IsPromotableTo(fromType, toType); } } private static bool UntypedNullAwareIsStructurallyEqual(TypeUsage fromType, TypeUsage toType) { if (fromType == null) { return UntypedNullAwareIsPromotableTo(fromType, toType); } else { return TypeSemantics.IsStructurallyEqual(fromType, toType); } } #endregion } /// /// Represents an utility for creating anonymous IDisposable implementations. /// internal class Disposer : IDisposable { private readonly Action _action; internal Disposer(Action action) { Debug.Assert(action != null, "action != null"); _action = action; } public void Dispose() { _action(); GC.SuppressFinalize(this); } } internal enum GroupAggregateKind { None, /// /// Inside of an aggregate function (Max, Min, etc). /// All range variables originating on the defining scope of this aggregate should yield . /// Function, /// /// Inside of GROUPPARTITION expression. /// All range variables originating on the defining scope of this aggregate should yield . /// Partition, /// /// Inside of a group key definition /// All range variables originating on the defining scope of this aggregate should yield . /// GroupKey } /// /// Represents group aggregate information during aggregate construction/resolution. /// internal abstract class GroupAggregateInfo { protected GroupAggregateInfo( GroupAggregateKind aggregateKind, AST.GroupAggregateExpr astNode, ErrorContext errCtx, GroupAggregateInfo containingAggregate, ScopeRegion definingScopeRegion) { Debug.Assert(aggregateKind != GroupAggregateKind.None, "aggregateKind != GroupAggregateKind.None"); Debug.Assert(errCtx != null, "errCtx != null"); Debug.Assert(definingScopeRegion != null, "definingScopeRegion != null"); AggregateKind = aggregateKind; AstNode = astNode; ErrCtx = errCtx; DefiningScopeRegion = definingScopeRegion; SetContainingAggregate(containingAggregate); } protected void AttachToAstNode(string aggregateName, TypeUsage resultType) { Debug.Assert(AstNode != null, "AstNode must be set."); Debug.Assert(aggregateName != null && resultType != null, "aggregateName and aggregateDefinition must not be null."); Debug.Assert(AggregateName == null && AggregateStubExpression == null, "Cannot reattach."); AggregateName = aggregateName; AggregateStubExpression = resultType.Null(); // Attach group aggregate info to the ast node. AstNode.AggregateInfo = this; } internal void DetachFromAstNode() { Debug.Assert(AstNode != null, "AstNode must be set."); AstNode.AggregateInfo = null; } /// /// Updates referenced scope index of the aggregate. /// Function call is not allowed after has been called. /// internal void UpdateScopeIndex(int referencedScopeIndex, SemanticResolver sr) { Debug.Assert(_evaluatingScopeRegion == null, "Can not update referenced scope index after _evaluatingScopeRegion have been computed."); ScopeRegion referencedScopeRegion = sr.GetDefiningScopeRegion(referencedScopeIndex); if (_innermostReferencedScopeRegion == null || _innermostReferencedScopeRegion.ScopeRegionIndex < referencedScopeRegion.ScopeRegionIndex) { _innermostReferencedScopeRegion = referencedScopeRegion; } } /// /// Gets/sets the innermost referenced scope region of the current aggregate. /// This property is used to save/restore the scope region value during a potentially throw-away attempt to /// convert an as a collection function in the method. /// Setting the value is not allowed after has been called. /// internal ScopeRegion InnermostReferencedScopeRegion { get { return _innermostReferencedScopeRegion; } set { Debug.Assert(_evaluatingScopeRegion == null, "Can't change _innermostReferencedScopeRegion after _evaluatingScopeRegion has been initialized."); _innermostReferencedScopeRegion = value; } } private ScopeRegion _innermostReferencedScopeRegion; /// /// Validates the aggregate info and computes property. /// Seals the aggregate info object (no more AddContainedAggregate(...), RemoveContainedAggregate(...) and UpdateScopeIndex(...) calls allowed). /// internal void ValidateAndComputeEvaluatingScopeRegion(SemanticResolver sr) { Debug.Assert(_evaluatingScopeRegion == null, "_evaluatingScopeRegion has already been initialized"); // // If _innermostReferencedScopeRegion is null, it means the aggregate is not correlated (a constant value), // so resolve it to the DefiningScopeRegion. // _evaluatingScopeRegion = _innermostReferencedScopeRegion ?? DefiningScopeRegion; if (!_evaluatingScopeRegion.IsAggregating) { // // In some cases the found scope region does not aggregate (has no grouping). So adding the aggregate to that scope won't work. // In this situation we need to backtrack from the found region to the first inner region that performs aggregation. // Example: // select yy.cx, yy.cy, yy.cz // from {1, 2} as x cross apply (select zz.cx, zz.cy, zz.cz // from {3, 4} as y cross apply (select Count(x) as cx, Count(y) as cy, Count(z) as cz // from {5, 6} as z) as zz // ) as yy // Note that Count aggregates cx and cy refer to scope regions that do aggregate. All three aggregates needs to be added to the only // aggregating region - the innermost. // int scopeRegionIndex = _evaluatingScopeRegion.ScopeRegionIndex; _evaluatingScopeRegion = null; foreach (ScopeRegion innerSR in sr.ScopeRegions.Skip(scopeRegionIndex)) { if (innerSR.IsAggregating) { _evaluatingScopeRegion = innerSR; break; } } if (_evaluatingScopeRegion == null) { throw EntityUtil.EntitySqlError(Strings.GroupVarNotFoundInScope); } } // // Validate all the contained aggregates for violation of the containment rule: // None of the nested (contained) aggregates must be evaluating on a scope region that is // a. equal or inner to the evaluating scope of the current aggregate and // b. equal or outer to the defining scope of the current aggregate. // // Example of a disallowed query: // // select // (select max(x + max(y)) // from {1} as y) // from {0} as x // // Example of an allowed query where the ESR of the nested aggregate is outer to the ESR of the outer aggregate: // // select // (select max(y + max(x)) // from {1} as y) // from {0} as x // // Example of an allowed query where the ESR of the nested aggregate is inner to the DSR of the outer aggregate: // // select max(x + anyelement(select value max(y) from {1} as y)) // from {0} as x // Debug.Assert(_evaluatingScopeRegion.IsAggregating, "_evaluatingScopeRegion.IsAggregating must be true"); Debug.Assert(_evaluatingScopeRegion.ScopeRegionIndex <= DefiningScopeRegion.ScopeRegionIndex, "_evaluatingScopeRegion must outer to the DefiningScopeRegion"); ValidateContainedAggregates(_evaluatingScopeRegion.ScopeRegionIndex, DefiningScopeRegion.ScopeRegionIndex); } /// /// Recursively validates that of all contained aggregates /// is outside of the range of scope regions defined by and . /// Throws in the case of violation. /// private void ValidateContainedAggregates(int outerBoundaryScopeRegionIndex, int innerBoundaryScopeRegionIndex) { if (_containedAggregates != null) { foreach (GroupAggregateInfo containedAggregate in _containedAggregates) { if (containedAggregate.EvaluatingScopeRegion.ScopeRegionIndex >= outerBoundaryScopeRegionIndex && containedAggregate.EvaluatingScopeRegion.ScopeRegionIndex <= innerBoundaryScopeRegionIndex) { int line, column; string currentAggregateInfo = EntitySqlException.FormatErrorContext( ErrCtx.CommandText, ErrCtx.InputPosition, ErrCtx.ErrorContextInfo, ErrCtx.UseContextInfoAsResourceIdentifier, out line, out column); string nestedAggregateInfo = EntitySqlException.FormatErrorContext( containedAggregate.ErrCtx.CommandText, containedAggregate.ErrCtx.InputPosition, containedAggregate.ErrCtx.ErrorContextInfo, containedAggregate.ErrCtx.UseContextInfoAsResourceIdentifier, out line, out column); throw EntityUtil.EntitySqlError(Strings.NestedAggregateCannotBeUsedInAggregate(nestedAggregateInfo, currentAggregateInfo)); } // // We need to check the full subtree in order to catch this case: // select max(x + // anyelement(select max(y + // anyelement(select value max(x) // from {2} as z)) // from {1} as y)) // from {0} as x // containedAggregate.ValidateContainedAggregates(outerBoundaryScopeRegionIndex, innerBoundaryScopeRegionIndex); } } } internal void SetContainingAggregate(GroupAggregateInfo containingAggregate) { if (_containingAggregate != null) { // // Aggregates in this query // // select value max(anyelement(select value max(b + max(a + anyelement(select value c1 // from {2} as c group by c as c1))) // from {1} as b group by b as b1)) // // from {0} as a group by a as a1 // // are processed in the following steps: // 1. the outermost aggregate (max1) begins processing as a collection function; // 2. the middle aggregate (max2) begins processing as a collection function; // 3. the innermost aggregate (max3) is processed as a collection function; // 4. max3 is reprocessed as an aggregate; it does not see any containing aggregates at this point, so it's not wired up; // max3 is validated and sealed; // evaluating scope region for max3 is the outermost scope region, to which it gets assigned; // max3 aggregate info object is attached to the corresponding AST node; // 5. max2 completes processing as a collection function and begins processing as an aggregate; // 6. max3 is reprocessed as an aggregate in the SemanticAnalyzer.TryConvertAsResolvedGroupAggregate(...) method, and // wired up to max2 as contained/containing; // 7. max2 completes processing as an aggregate; // max2 is validated and sealed; // note that max2 does not see any containing aggregates at this point, so it's wired up only to max3; // evaluating scope region for max2 is the middle scope region to which it gets assigned; // 6. middle scope region completes processing, yields a DbExpression and cleans up all aggregate info objects assigned to it (max2); // max2 is detached from the corresponding AST node; // at this point max3 is still assigned to the outermost scope region and still wired to the dropped max2 as containing/contained; // 7. max1 completes processing as a collection function and begins processing as an aggregate; // 8. max2 is revisited and begins processing as a collection function (note that because the old aggregate info object for max2 was dropped // and detached from the AST node in step 6, SemanticAnalyzer.TryConvertAsResolvedGroupAggregate(...) does not recognize max2 as an aggregate); // 9. max3 is recognized as an aggregate in the SemanticAnalyzer.TryConvertAsResolvedGroupAggregate(...) method; // max3 is rewired from the dropped max2 (step 6) to max1 as contained/containing, now max1 and max3 are wired as containing/contained; // 10. max2 completes processing as a collection function and begins processing as an aggregate; // max2 sees max1 as a containing aggregate and wires to it; // 11. max3 is reprocessed as resolved aggregate inside of TryConvertAsResolvedGroupAggregate(...) method; // max3 is rewired from max1 to max2 as containing/contained aggregate; // 12. at this point max1 is wired to max2 and max2 is wired to max3, the tree is correct; // // ... both max1 and max3 are assigned to the same scope for evaluation, this is detected and an error is reported; // // // Remove this aggregate from the old containing aggregate before rewiring to the new parent. // _containingAggregate.RemoveContainedAggregate(this); } // // Accept the new parent and wire to it as a contained aggregate. // _containingAggregate = containingAggregate; if (_containingAggregate != null) { _containingAggregate.AddContainedAggregate(this); } } /// /// Function call is not allowed after has been called. /// Adding new contained aggregate may invalidate the current aggregate. /// private void AddContainedAggregate(GroupAggregateInfo containedAggregate) { Debug.Assert(_evaluatingScopeRegion == null, "Can not add contained aggregate after _evaluatingScopeRegion have been computed."); if (_containedAggregates == null) { _containedAggregates = new List(); } Debug.Assert(_containedAggregates.Contains(containedAggregate) == false, "containedAggregate is already registered"); _containedAggregates.Add(containedAggregate); } private List _containedAggregates; /// /// Function call is _allowed_ after has been called. /// Removing contained aggregates cannot invalidate the current aggregate. /// /// Consider the following query: /// /// select value max(a + anyelement(select value max(b + max(a + anyelement(select value c1 /// from {2} as c group by c as c1))) /// from {1} as b group by b as b1)) /// from {0} as a group by a as a1 /// /// Outer aggregate - max1, middle aggregate - max2, inner aggregate - max3. /// In this query after max1 have been processed as a collection function, max2 and max3 are wired as containing/contained. /// There is a point later when max1 is processed as an aggregate, max2 is processed as a collection function and max3 is processed as /// an aggregate. Note that at this point the "aggregate" version of max2 is dropped and detached from the AST node when the middle scope region /// completes processing; also note that because evaluating scope region of max3 is the outer scope region, max3 aggregate info is still attached to /// the AST node and it is still wired to the dropped aggregate info object of max2. At this point max3 does not see new max2 as a containing aggregate, /// and it rewires to max1, during this rewiring it needs to to remove itself from the old max2 and add itself to max1. /// The old max2 at this point is sealed, so the removal is performed on the sealed object. /// private void RemoveContainedAggregate(GroupAggregateInfo containedAggregate) { Debug.Assert(_containedAggregates != null && _containedAggregates.Contains(containedAggregate), "_containedAggregates.Contains(containedAggregate)"); _containedAggregates.Remove(containedAggregate); } internal readonly GroupAggregateKind AggregateKind; /// /// Null when is created for a group key processing. /// internal readonly AST.GroupAggregateExpr AstNode; internal readonly ErrorContext ErrCtx; /// /// Scope region that contains the aggregate expression. /// internal readonly ScopeRegion DefiningScopeRegion; /// /// Scope region that evaluates the aggregate expression. /// internal ScopeRegion EvaluatingScopeRegion { get { // // _evaluatingScopeRegion is initialized in the ValidateAndComputeEvaluatingScopeRegion(...) method. // Debug.Assert(_evaluatingScopeRegion != null, "_evaluatingScopeRegion is not initialized"); return _evaluatingScopeRegion; } } private ScopeRegion _evaluatingScopeRegion; /// /// Parent aggregate expression that contains the current aggregate expression. /// May be null. /// internal GroupAggregateInfo ContainingAggregate { get { return _containingAggregate; } } private GroupAggregateInfo _containingAggregate; internal string AggregateName; internal DbNullExpression AggregateStubExpression; } internal sealed class FunctionAggregateInfo : GroupAggregateInfo { internal FunctionAggregateInfo(AST.MethodExpr methodExpr, ErrorContext errCtx, GroupAggregateInfo containingAggregate, ScopeRegion definingScopeRegion) : base(GroupAggregateKind.Function, methodExpr, errCtx, containingAggregate, definingScopeRegion) { Debug.Assert(methodExpr != null, "methodExpr != null"); } internal void AttachToAstNode(string aggregateName, DbAggregate aggregateDefinition) { Debug.Assert(aggregateDefinition != null, "aggregateDefinition != null"); base.AttachToAstNode(aggregateName, aggregateDefinition.ResultType); AggregateDefinition = aggregateDefinition; } internal DbAggregate AggregateDefinition; } internal sealed class GroupPartitionInfo : GroupAggregateInfo { internal GroupPartitionInfo(AST.GroupPartitionExpr groupPartitionExpr, ErrorContext errCtx, GroupAggregateInfo containingAggregate, ScopeRegion definingScopeRegion) : base(GroupAggregateKind.Partition, groupPartitionExpr, errCtx, containingAggregate, definingScopeRegion) { Debug.Assert(groupPartitionExpr != null, "groupPartitionExpr != null"); } internal void AttachToAstNode(string aggregateName, DbExpression aggregateDefinition) { Debug.Assert(aggregateDefinition != null, "aggregateDefinition != null"); base.AttachToAstNode(aggregateName, aggregateDefinition.ResultType); AggregateDefinition = aggregateDefinition; } internal DbExpression AggregateDefinition; } internal sealed class GroupKeyAggregateInfo : GroupAggregateInfo { internal GroupKeyAggregateInfo(GroupAggregateKind aggregateKind, ErrorContext errCtx, GroupAggregateInfo containingAggregate, ScopeRegion definingScopeRegion) : base(aggregateKind, null /* there is no AST.GroupAggregateExpression corresponding to the group key */, errCtx, containingAggregate, definingScopeRegion) { } } internal abstract class InlineFunctionInfo { internal InlineFunctionInfo(AST.FunctionDefinition functionDef, List parameters) { FunctionDefAst = functionDef; Parameters = parameters; } internal readonly AST.FunctionDefinition FunctionDefAst; internal readonly List Parameters; internal abstract DbLambda GetLambda(SemanticResolver sr); } internal sealed class ScopeRegion { private readonly ScopeManager _scopeManager; internal ScopeRegion(ScopeManager scopeManager, int firstScopeIndex, int scopeRegionIndex) { _scopeManager = scopeManager; _firstScopeIndex = firstScopeIndex; _scopeRegionIndex = scopeRegionIndex; } /// /// First scope of the region. /// internal int FirstScopeIndex { get { return _firstScopeIndex; } } private readonly int _firstScopeIndex; /// /// Index of the scope region. /// Outer scope regions have smaller index value than inner scope regions. /// internal int ScopeRegionIndex { get { return _scopeRegionIndex; } } private readonly int _scopeRegionIndex; /// /// True if given scope is in the current scope region. /// internal bool ContainsScope(int scopeIndex) { return (scopeIndex >= _firstScopeIndex); } /// /// Marks current scope region as performing group/folding operation. /// internal void EnterGroupOperation(DbExpressionBinding groupAggregateBinding) { Debug.Assert(!IsAggregating, "Scope region group operation is not reentrant."); _groupAggregateBinding = groupAggregateBinding; } /// /// Clears the flag on the group scope. /// internal void RollbackGroupOperation() { Debug.Assert(IsAggregating, "Scope region must inside group operation in order to leave it."); _groupAggregateBinding = null; } /// /// True when the scope region performs group/folding operation. /// internal bool IsAggregating { get { return _groupAggregateBinding != null; } } internal DbExpressionBinding GroupAggregateBinding { get { Debug.Assert(IsAggregating, "IsAggregating must be true."); return _groupAggregateBinding; } } private DbExpressionBinding _groupAggregateBinding; /// /// Returns list of group aggregates evaluated on the scope region. /// internal List GroupAggregateInfos { get { return _groupAggregateInfos; } } private List _groupAggregateInfos = new List(); /// /// Adds group aggregate name to the scope region. /// internal void RegisterGroupAggregateName(string groupAggregateName) { Debug.Assert(!_groupAggregateNames.Contains(groupAggregateName), "!_groupAggregateNames.ContainsKey(groupAggregateName)"); _groupAggregateNames.Add(groupAggregateName); } internal bool ContainsGroupAggregate(string groupAggregateName) { return _groupAggregateNames.Contains(groupAggregateName); } private HashSet _groupAggregateNames = new HashSet(); /// /// True if a recent expression resolution was correlated. /// internal bool WasResolutionCorrelated { get { return _wasResolutionCorrelated; } set { _wasResolutionCorrelated = value; } } private bool _wasResolutionCorrelated = false; /// /// Applies to all scope entries in the current scope region. /// internal void ApplyToScopeEntries(Action action) { Debug.Assert(FirstScopeIndex <= _scopeManager.CurrentScopeIndex, "FirstScopeIndex <= CurrentScopeIndex"); for (int i = FirstScopeIndex; i <= _scopeManager.CurrentScopeIndex; ++i) { foreach (KeyValuePair scopeEntry in _scopeManager.GetScopeByIndex(i)) { action(scopeEntry.Value); } } } /// /// Applies to all scope entries in the current scope region. /// internal void ApplyToScopeEntries(Func action) { Debug.Assert(FirstScopeIndex <= _scopeManager.CurrentScopeIndex, "FirstScopeIndex <= CurrentScopeIndex"); for (int i = FirstScopeIndex; i <= _scopeManager.CurrentScopeIndex; ++i) { Scope scope = _scopeManager.GetScopeByIndex(i); List> updatedEntries = null; foreach (KeyValuePair scopeEntry in scope) { ScopeEntry newScopeEntry = action(scopeEntry.Value); Debug.Assert(newScopeEntry != null, "newScopeEntry != null"); if (scopeEntry.Value != newScopeEntry) { if (updatedEntries == null) { updatedEntries = new List>(); } updatedEntries.Add(new KeyValuePair(scopeEntry.Key, newScopeEntry)); } } if (updatedEntries != null) { updatedEntries.ForEach((updatedScopeEntry) => scope.Replace(updatedScopeEntry.Key, updatedScopeEntry.Value)); } } } internal void RollbackAllScopes() { _scopeManager.RollbackToScope(FirstScopeIndex - 1); } } /// /// Represents a pair of types to avoid uncessary enumerations to split kvp elements /// internal sealed class Pair { internal Pair(L left, R right) { Left = left; Right = right; } internal L Left; internal R Right; internal KeyValuePair GetKVP() { return new KeyValuePair(Left, Right); } } }