diff --git a/src/libraries/Microsoft.PowerFx.Core/App/IExternalEnabledFeatures.cs b/src/libraries/Microsoft.PowerFx.Core/App/IExternalEnabledFeatures.cs index 52cd752720..8599f838b7 100644 --- a/src/libraries/Microsoft.PowerFx.Core/App/IExternalEnabledFeatures.cs +++ b/src/libraries/Microsoft.PowerFx.Core/App/IExternalEnabledFeatures.cs @@ -36,6 +36,6 @@ internal sealed class DefaultEnabledFeatures : IExternalEnabledFeatures public bool IsEnhancedComponentFunctionPropertyEnabled => true; - public bool IsComponentFunctionPropertyDataflowEnabled => true; + public bool IsComponentFunctionPropertyDataflowEnabled => true; } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs index 5778346a41..9abf51f816 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs @@ -1167,7 +1167,7 @@ private bool SetErrorForMismatchedColumnsCore(DType expectedType, DType actualTy } // Second, set column missing message if applicable - if (RequireAllParamColumns && !expectedType.AreFieldsOptional) + if ((RequireAllParamColumns || features.PowerFxV1CompatibilityRules) && !expectedType.AreFieldsOptional) { errors.EnsureError( DocumentErrorSeverity.Severe, diff --git a/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs b/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs index 0bb73cce3a..b5ee24186a 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs @@ -523,8 +523,10 @@ internal static class TexlStrings public static StringGetter AboutClearCollect = (b) => StringResources.Get("AboutClearCollect", b); public static StringGetter AboutRemove = (b) => StringResources.Get("AboutRemove", b); - public static StringGetter RemoveDataSourceArg = (b) => StringResources.Get("RemoveDataSourceArg", b); - public static StringGetter RemoveRecordsArg = (b) => StringResources.Get("RemoveRecordsArg", b); + public static StringGetter RemoveArg1 = (b) => StringResources.Get("RemoveArg1", b); + public static StringGetter RemoveArg2 = (b) => StringResources.Get("RemoveArg2", b); + public static StringGetter RemoveArg3 = (b) => StringResources.Get("RemoveArg3", b); + public static StringGetter RemoveAllArg2 = (b) => StringResources.Get("RemoveAllArg2", b); public static StringGetter AboutDec2Hex = (b) => StringResources.Get("AboutDec2Hex", b); public static StringGetter Dec2HexArg1 = (b) => StringResources.Get("Dec2HexArg1", b); @@ -655,6 +657,7 @@ internal static class TexlStrings public static ErrorResourceKey ErrBadType_ExpectedType_ProvidedType = new ErrorResourceKey("ErrBadType_ExpectedType_ProvidedType"); public static ErrorResourceKey ErrBadType_VoidExpression = new ErrorResourceKey("ErrBadType_VoidExpression"); public static ErrorResourceKey ErrBadSchema_ExpectedType = new ErrorResourceKey("ErrBadSchema_ExpectedType"); + public static ErrorResourceKey ErrNeedTable_Arg = new ErrorResourceKey("ErrNeedTable_Arg"); public static ErrorResourceKey ErrInvalidArgs_Func = new ErrorResourceKey("ErrInvalidArgs_Func"); public static ErrorResourceKey ErrNeedTable_Func = new ErrorResourceKey("ErrNeedTable_Func"); public static ErrorResourceKey ErrNeedTableCol_Func = new ErrorResourceKey("ErrNeedTableCol_Func"); @@ -868,5 +871,9 @@ internal static class TexlStrings public static ErrorResourceKey ErrJoinArgIsNotAsNode = new ErrorResourceKey("ErrJoinArgIsNotAsNode"); public static ErrorResourceKey ErrJoinAtLeastOneRigthRecordField = new ErrorResourceKey("ErrJoinAtLeastOneRigthRecordField"); public static ErrorResourceKey ErrJoinDottedNameleft = new ErrorResourceKey("ErrJoinDottedNameleft"); + + public static ErrorResourceKey ErrCollectionDoesNotAcceptThisType = new ErrorResourceKey("ErrCollectionDoesNotAcceptThisType"); + public static ErrorResourceKey ErrNeedAll = new ErrorResourceKey("ErrNeedAll"); + public static ErrorResourceKey ErrNeedCollection_Func = new ErrorResourceKey("ErrNeedCollection_Func"); } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Config/Features.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Config/Features.cs index abf908f462..7d7e92e3d7 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Config/Features.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Config/Features.cs @@ -82,6 +82,11 @@ public sealed class Features /// internal bool IsUserDefinedTypesEnabled { get; init; } = false; + /// + /// Enables RemoveAll delegation. + /// + internal bool IsRemoveAllDelegationEnabled { get; init; } + internal static readonly Features None = new Features(); /// @@ -124,6 +129,7 @@ internal Features(Features other) AsTypeLegacyCheck = other.AsTypeLegacyCheck; JsonFunctionAcceptsLazyTypes = other.JsonFunctionAcceptsLazyTypes; IsLookUpReductionDelegationEnabled = other.IsLookUpReductionDelegationEnabled; + IsRemoveAllDelegationEnabled = other.IsRemoveAllDelegationEnabled; } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs index fee7d280c7..f5ffc29e48 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs @@ -184,7 +184,7 @@ public override DValue Last(bool mutationCopy = false) public override async Task> RemoveAsync(IEnumerable recordsToRemove, bool all, CancellationToken cancellationToken) { var ret = false; - var deleteList = new List(); + var markedToDeletionIndexes = new HashSet(); var errors = new List(); cancellationToken.ThrowIfCancellationRequested(); @@ -194,21 +194,30 @@ public override async Task> RemoveAsync(IEnumerable dRecord = Marshal(item); if (await MatchesAsync(dRecord.Value, recordToRemove, cancellationToken).ConfigureAwait(false)) { - found = true; - - deleteList.Add(item); + if (markedToDeletionIndexes.Contains(i)) + { + continue; + } + else + { + found = true; + markedToDeletionIndexes.Add(i); + } if (!all) { @@ -220,13 +229,13 @@ public override async Task> RemoveAsync(IEnumerable true; + + public override bool ModifiesValues => true; + + public override bool CanSuggestInputColumns => true; + + public override bool IsSelfContained => false; + + public override bool RequiresDataSourceScope => true; + + public override bool SupportsParamCoercion => false; + + public override RequiredDataSourcePermissions FunctionPermission => RequiredDataSourcePermissions.Delete; + + public override bool MutatesArg(int argIndex, TexlNode arg) => argIndex == 0; + + public override bool TryGetTypeForArgSuggestionAt(int argIndex, out DType type) + { + if (argIndex > 0) + { + type = default; + return false; + } + + return base.TryGetTypeForArgSuggestionAt(argIndex, out type); + } + + public RemoveBaseFunction(int arityMax, params DType[] paramTypes) + : base("Remove", TexlStrings.AboutRemove, FunctionCategories.Behavior, DType.EmptyTable, 0, 2, arityMax, paramTypes) + { + } + + public override bool IsLazyEvalParam(TexlNode node, int index, Features features) + { + // First argument to mutation functions is Lazy for datasources that are copy-on-write. + // If there are any side effects in the arguments, we want those to have taken place before we make the copy. + return index == 0; + } + + public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[] argTypes, IErrorContainer errors) + { + base.CheckSemantics(binding, args, argTypes, errors); + base.ValidateArgumentIsMutable(binding, args[0], errors); + MutationUtils.CheckSemantics(binding, this, args, argTypes, errors); + } + + public override IEnumerable GetIdentifierOfModifiedValue(TexlNode[] args, out TexlNode identifierNode) + { + Contracts.AssertValue(args); + + identifierNode = null; + if (args.Length == 0) + { + return null; + } + + var firstNameNode = args[0]?.AsFirstName(); + identifierNode = firstNameNode; + if (firstNameNode == null) + { + return null; + } + + var identifiers = new List + { + firstNameNode.Ident + }; + return identifiers; + } + + public override bool IsAsyncInvocation(CallNode callNode, TexlBinding binding) + { + Contracts.AssertValue(callNode); + Contracts.AssertValue(binding); + + return Arg0RequiresAsync(callNode, binding); + } + + public bool CheckEnumType(Features features, DType argType) + { + var enumValid = BuiltInEnums.RemoveFlagsEnum.FormulaType._type.Accepts(argType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: features.PowerFxV1CompatibilityRules); + + return (features.StronglyTypedBuiltinEnums && enumValid) || + (!features.StronglyTypedBuiltinEnums && (DType.String.Accepts(argType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: features.PowerFxV1CompatibilityRules) || enumValid)); + } + } + + // Remove(collection:*[], item1:![], item2:![], ..., ["All"]) + internal class RemoveFunction : RemoveBaseFunction, ISuggestionAwareFunction + { + public bool CanSuggestThisItem => true; + + // Return true if this function affects datasource query options. + public override bool AffectsDataSourceQueryOptions => true; + + public override bool ArgMatchesDatasourceType(int argNum) + { + return argNum >= 1; + } + + public RemoveFunction() + : base(int.MaxValue, DType.EmptyTable) + { + } + + public override IEnumerable GetSignatures() + { + yield return new[] { TexlStrings.RemoveArg1, TexlStrings.RemoveArg2 }; + yield return new[] { TexlStrings.RemoveArg1, TexlStrings.RemoveArg2, TexlStrings.RemoveArg2 }; + yield return new[] { TexlStrings.RemoveArg1, TexlStrings.RemoveArg2, TexlStrings.RemoveArg2, TexlStrings.RemoveArg2 }; + } + + public override IEnumerable GetSignatures(int arity) + { + if (arity > 2) + { + return GetGenericSignatures(arity, TexlStrings.RemoveArg1, TexlStrings.RemoveArg2); + } + + return base.GetSignatures(arity); + } + + public override IEnumerable GetRequiredEnumNames() + { + return new List() { LanguageConstants.RemoveFlagsEnumString }; + } + + public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary nodeToCoercedTypeMap) + { + Contracts.AssertValue(args); + Contracts.AssertAllValues(args); + Contracts.AssertValue(argTypes); + Contracts.Assert(args.Length == argTypes.Length); + Contracts.AssertValue(errors); + Contracts.Assert(MinArity <= args.Length && args.Length <= MaxArity); + + bool fValid = base.CheckTypes(context, args, argTypes, errors, out returnType, out nodeToCoercedTypeMap); + Contracts.Assert(returnType.IsTable); + + DType collectionType = argTypes[0]; + if (!collectionType.IsTable) + { + fValid = false; + errors.EnsureError(args[0], TexlStrings.ErrNeedCollection_Func, Name); + } + + int argCount = argTypes.Length; + for (int i = 1; i < argCount; i++) + { + DType argType = argTypes[i]; + + if (!argType.IsRecord) + { + if (argCount >= 3 && i == argCount - 1 && CheckEnumType(context.Features, argType)) + { + continue; + } + + fValid = false; + errors.EnsureError(args[i], TexlStrings.ErrNeedRecord_Arg, args[i]); + continue; + } + + var collectionAcceptsRecord = collectionType.Accepts(argType.ToTable(), exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules); + var recordAcceptsCollection = argType.ToTable().Accepts(collectionType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules); + + // PFxV1 is more restrictive than PA in terms of column matching. If the collection does not accept the record or vice versa, it is an error. + // The item schema should be compatible with the collection schema. + if ((context.Features.PowerFxV1CompatibilityRules && (!collectionAcceptsRecord || !recordAcceptsCollection)) || + (!context.Features.PowerFxV1CompatibilityRules && (!collectionAcceptsRecord && !recordAcceptsCollection))) + { + fValid = false; + if (!SetErrorForMismatchedColumns(collectionType, argType, args[i], errors, context.Features)) + { + errors.EnsureError(DocumentErrorSeverity.Severe, args[i], TexlStrings.ErrTableDoesNotAcceptThisType); + } + } + + // Only warn about no-op record inputs if there are no data sources that would use reference identity for comparison. + else if (!collectionType.AssociatedDataSources.Any() && !recordAcceptsCollection) + { + errors.EnsureError(DocumentErrorSeverity.Warning, args[i], TexlStrings.ErrCollectionDoesNotAcceptThisType); + } + } + + returnType = context.Features.PowerFxV1CompatibilityRules ? DType.Void : collectionType; + + return fValid; + } + + // This method returns true if there are special suggestions for a particular parameter of the function. + public override bool HasSuggestionsForParam(int argumentIndex) + { + Contracts.Assert(argumentIndex >= 0); + + return argumentIndex != 1; + } + + /// + /// As Remove uses the source record in it's entirity to find the entry in table, uses deepcompare at runtime, we need all fields from source. + /// So update the selects for all columns in the source in this case except when datasource is pageable. + /// In that case, we can get the info at runtime. + /// + public override bool UpdateDataQuerySelects(CallNode callNode, TexlBinding binding, DataSourceToQueryOptionsMap dataSourceToQueryOptionsMap) + { + Contracts.AssertValue(callNode); + Contracts.AssertValue(binding); + + if (!CheckArgsCount(callNode, binding)) + { + return false; + } + + var args = Contracts.VerifyValue(callNode.Args.Children); + + DType dsType = binding.GetType(args[0]); + if (dsType.AssociatedDataSources == null + || dsType == DType.EmptyTable) + { + return false; + } + + var sourceRecordType = binding.GetType(args[1]); + + // This might be the case where Remove(CDS, Gallery.Selected) + if (sourceRecordType == DType.EmptyRecord) + { + return false; + } + + var firstTypeName = sourceRecordType.GetNames(DPath.Root).FirstOrDefault(); + + if (!firstTypeName.IsValid) + { + return false; + } + + DType type = firstTypeName.Type; + DName columnName = firstTypeName.Name; + + // This might be the case where Remove(CDS, Gallery.Selected) + if (!dsType.Contains(columnName)) + { + return false; + } + + dsType.AssociateDataSourcesToSelect( + dataSourceToQueryOptionsMap, + columnName, + type, + false /*skipIfNotInSchema*/, + true); /*skipExpands*/ + + return true; + } + + // This method filters for a table as the first parameter, records as intermediate parameters + // and a string (First/All) as the final parameter. + public override bool IsSuggestionTypeValid(int paramIndex, DType type) + { + Contracts.Assert(paramIndex >= 0); + Contracts.AssertValid(type); + + if (paramIndex >= MaxArity) + { + return false; + } + + if (paramIndex == 0) + { + return type.IsTable; + } + + // String suggestions for column names may occur within the context of a record + return type.IsRecord || type.Kind == DKind.String; + } + + protected override bool RequiresPagedDataForParamCore(TexlNode[] args, int paramIndex, TexlBinding binding) + { + Contracts.AssertValue(args); + Contracts.AssertAllValues(args); + Contracts.Assert(paramIndex >= 0 && paramIndex < args.Length); + Contracts.AssertValue(binding); + Contracts.Assert(binding.IsPageable(Contracts.VerifyValue(args[paramIndex]))); + + // For the first argument, we need only metadata. No actual data from datasource is required. + return paramIndex > 0; + } + } + + // Remove(collection:*[], source:*[], ["All"]) + internal class RemoveAllFunction : RemoveBaseFunction + { + public override bool ArgMatchesDatasourceType(int argNum) + { + return argNum == 1; + } + + public RemoveAllFunction() + : base(3, DType.EmptyTable, DType.EmptyTable) + { + } + + public override IEnumerable GetSignatures() + { + yield return new[] { TexlStrings.RemoveArg1, TexlStrings.RemoveAllArg2 }; + yield return new[] { TexlStrings.RemoveArg1, TexlStrings.RemoveAllArg2, TexlStrings.RemoveArg3 }; + } + + public override IEnumerable GetRequiredEnumNames() + { + return new List() { LanguageConstants.RemoveFlagsEnumString }; + } + + public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary nodeToCoercedTypeMap) + { + Contracts.AssertValue(args); + Contracts.AssertAllValues(args); + Contracts.AssertValue(argTypes); + Contracts.Assert(args.Length == argTypes.Length); + Contracts.AssertValue(errors); + Contracts.Assert(MinArity <= args.Length && args.Length <= MaxArity); + + bool fValid = base.CheckTypes(context, args, argTypes, errors, out returnType, out nodeToCoercedTypeMap); + Contracts.Assert(returnType.IsTable); + + DType collectionType = argTypes[0]; + if (!collectionType.IsTable) + { + fValid = false; + errors.EnsureError(args[0], TexlStrings.ErrNeedTable_Func, Name); + } + + // The source to be collected must be a table. + DType sourceType = argTypes[1]; + if (!sourceType.IsTable) + { + fValid = false; + errors.EnsureError(args[1], TexlStrings.ErrNeedTable_Arg, args[1]); + } + + // The source schema should be compatible with the collection schema. + if (!collectionType.Accepts(sourceType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules) && !sourceType.Accepts(collectionType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) + { + fValid = false; + if (!SetErrorForMismatchedColumns(collectionType, sourceType, args[1], errors, context.Features)) + { + errors.EnsureError(DocumentErrorSeverity.Severe, args[1], TexlStrings.ErrCollectionDoesNotAcceptThisType); + } + } + + if (args.Length == 3 && !CheckEnumType(context.Features, argTypes[2])) + { + fValid = false; + errors.EnsureError(DocumentErrorSeverity.Severe, args[2], TexlStrings.ErrRemoveAllArg); + } + + returnType = context.Features.PowerFxV1CompatibilityRules ? DType.Void : collectionType; + + return fValid; + } + + // This method returns true if there are special suggestions for a particular parameter of the function. + public override bool HasSuggestionsForParam(int argumentIndex) + { + Contracts.Assert(argumentIndex >= 0); + + return argumentIndex == 0; + } + + public override bool IsServerDelegatable(CallNode callNode, TexlBinding binding) + { + Contracts.AssertValue(callNode); + Contracts.AssertValue(binding); + + if (!CheckArgsCount(callNode, binding)) + { + return false; + } + + // Use ECS flag as a guard. + if (!binding.Features.IsRemoveAllDelegationEnabled) + { + return false; + } + + if (!binding.TryGetDataSourceInfo(callNode.Args.Children[0], out IExternalDataSource dataSource)) + { + return false; + } + + // Currently we delegate only to CDS. This is the first version of the feature and not a limitation of other datasources + if (dataSource == null || dataSource?.Kind != DataSourceKind.CdsNative) + { + TrackingProvider.Instance.SetDelegationTrackerStatus(DelegationStatus.DataSourceNotDelegatable, callNode, binding, this); + return false; + } + + // Right now we delegate only if the set of records is a table/queried table to mitigate the performance impact of the remove operation. + // Deleting single records (via Lookup) does not have the same performance impact + var dsType = binding.GetType(callNode.Args.Children[1]).Kind; + if (dsType != DKind.Table) + { + TrackingProvider.Instance.SetDelegationTrackerStatus(DelegationStatus.InvalidArgType, callNode, binding, this); + return false; + } + + TrackingProvider.Instance.SetDelegationTrackerStatus(DelegationStatus.DelegationSuccessful, callNode, binding, this); + return true; + } + + public override bool SupportsPaging(CallNode callNode, TexlBinding binding) + { + if (!binding.TryGetDataSourceInfo(callNode.Args.Children[0], out IExternalDataSource dataSource)) + { + return false; + } + + // Currently we delegate only to CDS. This is the first version of the feature and not a limitation of other datasources + if (dataSource == null || dataSource?.Kind == DataSourceKind.CdsNative) + { + return false; + } + + return base.SupportsPaging(callNode, binding); + } + } +} diff --git a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs index e560eb2c06..ddbfc8f736 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs @@ -210,5 +210,14 @@ private static Dictionary TraceSeverityDictionary() { "Right", "right" }, { "Full", "full" }, }); + + public static readonly EnumSymbol RemoveFlagsEnum = new EnumSymbol( + new DName(LanguageConstants.RemoveFlagsEnumString), + DType.String, + new Dictionary() + { + { "First", "first" }, + { "All", "all" }, + }); } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs index 435a06a989..8d1159fed1 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs @@ -31,6 +31,7 @@ internal sealed class EnumStoreBuilder { LanguageConstants.TraceSeverityEnumString, BuiltInEnums.TraceSeverityEnum }, { LanguageConstants.TraceOptionsEnumString, BuiltInEnums.TraceOptionsEnum }, { LanguageConstants.JoinTypeEnumString, BuiltInEnums.JoinTypeEnum }, + { LanguageConstants.RemoveFlagsEnumString, BuiltInEnums.RemoveFlagsEnum }, }; // DefaultEnums, with enum strings, is legacy and only used by Power Apps @@ -84,6 +85,10 @@ internal sealed class EnumStoreBuilder { LanguageConstants.JoinTypeEnumString, $"%s[{string.Join(", ", BuiltInEnums.JoinTypeEnum.EnumType.ValueTree.GetPairs().Select(pair => $@"{pair.Key}:""{pair.Value.Object}"""))}]" + }, + { + LanguageConstants.RemoveFlagsEnumString, + $"%s[{string.Join(", ", BuiltInEnums.RemoveFlagsEnum.EnumType.ValueTree.GetPairs().Select(pair => $@"{pair.Key}:""{pair.Value.Object}"""))}]" } }; #endregion diff --git a/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs b/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs index ca301dc4f8..ea341c9f2e 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs @@ -90,5 +90,10 @@ internal class LanguageConstants /// The string value representing the join type literal. /// public const string JoinTypeEnumString = "JoinType"; + + /// + /// The string value representing the remove flag option. + /// + public const string RemoveFlagsEnumString = "RemoveFlags"; } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs index 78be4b4db7..d8dd875a99 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs @@ -49,7 +49,8 @@ public static void EnableMutationFunctions(this SymbolTable symbolTable) symbolTable.AddFunction(new PatchSingleRecordImpl()); symbolTable.AddFunction(new PatchAggregateImpl()); symbolTable.AddFunction(new PatchAggregateSingleTableImpl()); - symbolTable.AddFunction(new RemoveFunction()); + symbolTable.AddFunction(new RemoveImpl()); + symbolTable.AddFunction(new RemoveAllImpl()); symbolTable.AddFunction(new ClearImpl()); symbolTable.AddFunction(new ClearCollectImpl()); symbolTable.AddFunction(new ClearCollectScalarImpl()); diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs index afe2e61a37..0848094203 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerFx.Core.Functions; using Microsoft.PowerFx.Core.IR; using Microsoft.PowerFx.Functions; using Microsoft.PowerFx.Interpreter.Localization; @@ -423,4 +422,22 @@ public async Task InvokeAsync(FunctionInvokeInfo invokeInfo, Cance return await new ClearCollectImpl().InvokeAsync(invokeInfo, cancellationToken).ConfigureAwait(false); } } + + // Remove(collection:*[], item1:![], item2:![], ..., ["All"]) + internal class RemoveImpl : RemoveFunction, IFunctionInvoker + { + public async Task InvokeAsync(FunctionInvokeInfo invokeInfo, CancellationToken cancellationToken) + { + return await MutationUtils.RemoveCore(invokeInfo, cancellationToken).ConfigureAwait(false); + } + } + + // Remove(collection:*[], source:*[], ["All"]) + internal class RemoveAllImpl : RemoveAllFunction, IFunctionInvoker + { + public async Task InvokeAsync(FunctionInvokeInfo invokeInfo, CancellationToken cancellationToken) + { + return await MutationUtils.RemoveCore(invokeInfo, cancellationToken).ConfigureAwait(false); + } + } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs index b970442c79..4d06427f68 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.PowerFx.Core.App.ErrorContainers; -using Microsoft.PowerFx.Core.Entities; -using Microsoft.PowerFx.Core.Errors; -using Microsoft.PowerFx.Core.Localization; -using Microsoft.PowerFx.Core.Types; -using Microsoft.PowerFx.Syntax; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerFx.Functions; +using Microsoft.PowerFx.Interpreter.Localization; using Microsoft.PowerFx.Types; namespace Microsoft.PowerFx.Interpreter @@ -47,5 +45,90 @@ public static DValue MergeRecords(IEnumerable records return DValue.Of(FormulaValue.NewRecordFromFields(mergedFields.Select(kvp => new NamedValue(kvp.Key, kvp.Value)))); } + + public static async Task RemoveCore(FunctionInvokeInfo invokeInfo, CancellationToken cancellationToken) + { + var args = invokeInfo.Args.ToArray(); + cancellationToken.ThrowIfCancellationRequested(); + + FormulaValue arg0; + + if (args[0] is LambdaFormulaValue arg0lazy) + { + arg0 = await arg0lazy.EvalAsync().ConfigureAwait(false); + } + else + { + arg0 = args[0]; + } + + if (arg0 is BlankValue || arg0 is ErrorValue) + { + return arg0; + } + + // If any of the argN (N>0) is error, return the error. + foreach (var arg in args.Skip(1)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (arg is ErrorValue) + { + return arg; + } + + if (arg is TableValue tableValue) + { + var errorRecord = tableValue.Rows.FirstOrDefault(row => row.IsError); + if (errorRecord != null) + { + return errorRecord.Error; + } + } + } + + var all = false; + var datasource = (TableValue)arg0; + + if (args.Count() >= 3 && args.Last() is OptionSetValue opv) + { + all = opv.Option == "All"; + } + + List recordsToRemove = null; + + if (args[1] is TableValue sourceTable) + { + recordsToRemove = sourceTable.Rows.Select(row => row.Value).ToList(); + } + else + { + recordsToRemove = args + .Skip(1) + .Where(arg => arg is RecordValue) + .OfType() + .ToList(); + } + + // At this point all errors have been handled. + var response = await datasource.RemoveAsync(recordsToRemove, all, cancellationToken).ConfigureAwait(false); + + if (response.IsError) + { + var errors = new List(); + foreach (var error in response.Error.Errors) + { + errors.Add(new ExpressionError() + { + ResourceKey = RuntimeStringResources.ErrRecordNotFound, + Kind = ErrorKind.NotFound + }); + } + + return new ErrorValue(invokeInfo.IRContext, errors); + } + + return invokeInfo.ReturnType == FormulaType.Void ? FormulaValue.NewVoid() : FormulaValue.NewBlank(); + } } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs deleted file mode 100644 index 7ce73eeba3..0000000000 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.PowerFx.Core.App.ErrorContainers; -using Microsoft.PowerFx.Core.Binding; -using Microsoft.PowerFx.Core.Errors; -using Microsoft.PowerFx.Core.Functions; -using Microsoft.PowerFx.Core.Localization; -using Microsoft.PowerFx.Core.Types; -using Microsoft.PowerFx.Core.Utils; -using Microsoft.PowerFx.Syntax; -using Microsoft.PowerFx.Types; -using static Microsoft.PowerFx.Core.Localization.TexlStrings; -using static Microsoft.PowerFx.Syntax.PrettyPrintVisitor; - -namespace Microsoft.PowerFx.Functions -{ - internal abstract class RemoveFunctionBase : BuiltinFunction - { - public override bool IsSelfContained => false; - - public override bool RequiresDataSourceScope => true; - - public override bool CanSuggestInputColumns => true; - - public override bool ManipulatesCollections => true; - - public override bool ArgMatchesDatasourceType(int argNum) - { - return argNum >= 1; - } - - public override bool MutatesArg(int argIndex, TexlNode arg) => argIndex == 0; - - public override bool IsLazyEvalParam(TexlNode node, int index, Features features) - { - // First argument to mutation functions is Lazy for datasources that are copy-on-write. - // If there are any side effects in the arguments, we want those to have taken place before we make the copy. - return index == 0; - } - - public RemoveFunctionBase(DPath theNamespace, string name, StringGetter description, FunctionCategories fc, DType returnType, BigInteger maskLambdas, int arityMin, int arityMax, params DType[] paramTypes) - : base(theNamespace, name, /*localeSpecificName*/string.Empty, description, fc, returnType, maskLambdas, arityMin, arityMax, paramTypes) - { - } - - public RemoveFunctionBase(string name, StringGetter description, FunctionCategories fc, DType returnType, BigInteger maskLambdas, int arityMin, int arityMax, params DType[] paramTypes) - : this(DPath.Root, name, description, fc, returnType, maskLambdas, arityMin, arityMax, paramTypes) - { - } - - protected static bool CheckArgs(IReadOnlyList args, out FormulaValue faultyArg) - { - // If any args are error, propagate up. - foreach (var arg in args) - { - if (arg is ErrorValue) - { - faultyArg = arg; - - return false; - } - } - - faultyArg = null; - - return true; - } - } - - internal class RemoveFunction : RemoveFunctionBase, IFunctionInvoker - { - public override bool IsSelfContained => false; - - public override bool TryGetTypeForArgSuggestionAt(int argIndex, out DType type) - { - if (argIndex == 1) - { - type = default; - return false; - } - - return base.TryGetTypeForArgSuggestionAt(argIndex, out type); - } - - public RemoveFunction() - : base("Remove", AboutRemove, FunctionCategories.Table | FunctionCategories.Behavior, DType.Unknown, 0, 2, int.MaxValue, DType.EmptyTable, DType.EmptyRecord) - { - } - - public override IEnumerable GetSignatures() - { - yield return new[] { RemoveDataSourceArg, RemoveRecordsArg }; - yield return new[] { RemoveDataSourceArg, RemoveRecordsArg, RemoveRecordsArg }; - } - - public override IEnumerable GetSignatures(int arity) - { - if (arity > 2) - { - return GetGenericSignatures(arity, RemoveDataSourceArg, RemoveRecordsArg, RemoveRecordsArg); - } - - return base.GetSignatures(arity); - } - - public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary nodeToCoercedTypeMap) - { - Contracts.AssertValue(args); - Contracts.AssertAllValues(args); - Contracts.AssertValue(argTypes); - Contracts.Assert(args.Length == argTypes.Length); - Contracts.AssertValue(errors); - Contracts.Assert(MinArity <= args.Length && args.Length <= MaxArity); - - var fValid = base.CheckTypes(context, args, argTypes, errors, out returnType, out nodeToCoercedTypeMap); - - DType collectionType = argTypes[0]; - if (!collectionType.IsTable) - { - errors.EnsureError(args[0], ErrNeedTable_Func, Name); - fValid = false; - } - - var argCount = argTypes.Length; - - for (var i = 1; i < argCount; i++) - { - DType argType = argTypes[i]; - - // The subsequent args should all be records. - if (!argType.IsRecord) - { - // The last arg may be the optional "ALL" parameter. - if (argCount >= 3 && i == argCount - 1 && DType.String.Accepts(argType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) - { - var strNode = (StrLitNode)args[i]; - - if (strNode.Value.ToUpperInvariant() != "ALL") - { - fValid = false; - errors.EnsureError(args[i], ErrRemoveAllArg, args[i]); - } - - continue; - } - - fValid = false; - errors.EnsureError(args[i], ErrNeedRecord, args[i]); - continue; - } - - var collectionAcceptsRecord = collectionType.Accepts(argType.ToTable(), exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules); - var recordAcceptsCollection = argType.ToTable().Accepts(collectionType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules); - - var featuresWithPFxV1RulesDisabled = new Features(context.Features) { PowerFxV1CompatibilityRules = false }; - bool checkAggregateNames = argType.CheckAggregateNames(collectionType, args[i], errors, featuresWithPFxV1RulesDisabled, SupportsParamCoercion); - - // The item schema should be compatible with the collection schema. - if (!checkAggregateNames) - { - fValid = false; - if (!SetErrorForMismatchedColumns(collectionType, argType, args[i], errors, context.Features)) - { - errors.EnsureError(DocumentErrorSeverity.Severe, args[i], ErrTableDoesNotAcceptThisType); - } - } - } - - returnType = context.Features.PowerFxV1CompatibilityRules ? DType.Void : collectionType; - - return fValid; - } - - public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[] argTypes, IErrorContainer errors) - { - base.CheckSemantics(binding, args, argTypes, errors); - base.ValidateArgumentIsMutable(binding, args[0], errors); - } - - public async Task InvokeAsync(FunctionInvokeInfo invokeInfo, CancellationToken cancellationToken) - { - var args = invokeInfo.Args; - var returnType = invokeInfo.ReturnType; - - var validArgs = CheckArgs(args, out FormulaValue faultyArg); - - if (!validArgs) - { - return faultyArg; - } - - var arg0lazy = (LambdaFormulaValue)args[0]; - var arg0 = await arg0lazy.EvalAsync().ConfigureAwait(false); - - if (arg0 is BlankValue) - { - return arg0; - } - - var argCount = args.Count(); - var lastArg = args.Last() as FormulaValue; - var all = false; - var toExclude = 1; - - if (argCount >= 3 && DType.String.Accepts(lastArg.Type._type, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: true)) - { - var lastArgValue = (string)lastArg.ToObject(); - - if (lastArgValue.ToUpperInvariant() == "ALL") - { - all = true; - toExclude = 2; - } - } - - var datasource = (TableValue)arg0; - var recordsToRemove = args.Skip(1).Take(args.Count - toExclude); - - cancellationToken.ThrowIfCancellationRequested(); - var ret = await datasource.RemoveAsync(recordsToRemove, all, cancellationToken).ConfigureAwait(false); - - // If the result is an error, propagate it up. else return blank. - FormulaValue result; - if (ret.IsError) - { - result = FormulaValue.NewError(ret.Error.Errors, returnType == FormulaType.Void ? FormulaType.Void : FormulaType.Blank); - } - else - { - result = returnType == FormulaType.Void ? FormulaValue.NewVoid() : FormulaValue.NewBlank(); - } - - return result; - } - } -} diff --git a/src/strings/PowerFxResources.en-US.resx b/src/strings/PowerFxResources.en-US.resx index bd38c3afe3..21957d5b81 100644 --- a/src/strings/PowerFxResources.en-US.resx +++ b/src/strings/PowerFxResources.en-US.resx @@ -3291,21 +3291,41 @@ function_parameter - Second argument to the Patch function - the updates to be applied to the given rows. - Removes a specific record or records from a data source + Removes (optionally All) items from the specified 'collection'. Description of 'Remove' function. - - data_source - function_parameter - First parameter for the Remove function. The data source that contains the records that you want to remove from. Translate this string. When translating, maintain as a single word (i.e., do not add spaces). + + collection + function_parameter - First parameter of the Remove function - the name of the collection to have an item removed. - - remove_record(s) - function_parameter - One or more records to be removed. Translate this string. When translating, maintain as a single word (i.e., do not add spaces). + + item + function_parameter - Second parameter to the Remove function - the item to be removed. If provided, last argument must be 'RemoveFlags.All'. Is there a typo? {Locked=RemoveFlags.All} Error Message, RemoveFlags.All is an enum value that does not get localized. + + The collection to remove rows from. + + + A record value specifying the row to remove. + + + source + function_parameter - Second parameter to the RemoveAll function - the source of the elements to be removed. + + + A table value that specifies multiple rows to remove from the given collection. + + + all + function_parameter - Third argument for the Remove function - value indicating whether to remove all matches or only the first one. + + + An optional argument that specifies whether to remove all matches, or just the first one. + Error Display text representing the Error value of NotificationType enum (NotificationType_Error_Name). This describes showing an error notification. The possible values for this enumeration are: Error, Warning, Success, Information. @@ -4922,4 +4942,60 @@ The '{0}' value is invalid in this context. It should be a reference to either '{1}' or '{2}' scope name. {Locked='RightRecord'} + + The first argument of '{0}' should be a collection. + Error Message. + + + Incompatible type. The collection can't contain values of this type. + Error Message. + + + You might need to convert the type of the item using, for example, a Datevalue, Text, or Value function. + 1 How to fix the error. + + + Article: Create and update a collection in a canvas app + Article: Create and update a collection in a canvas app. + + + https://go.microsoft.com/fwlink/?linkid=722609 + {Locked} + + + Module: Use basic formulas + 3 crown link on basic formulas + + + https://go.microsoft.com/fwlink/?linkid=2132396 + {Locked} + + + Module: Author basic formulas with tables and records + 3 crown link on tables and records + + + https://go.microsoft.com/fwlink/?linkid=2132700 + {Locked} + + + Incorrect argument. This formula expects an optional 'All' argument or no argument. + Error Message. + + + If you’re using an Update function, for example, you might supply an optional 'All' argument at the end of the formula. This feature is available because a record might occur more than once (e.g., in a collection) to make sure that all copies of a record are updated. + 1 How to fix the error. + + + Either supply the optional 'All' argument or remove it. + 1 How to fix the error. + + + Article: Formula reference for Power Apps + Article: Formula reference for Power Apps + + + https://go.microsoft.com/fwlink/?linkid=2132478 + {Locked} + \ No newline at end of file diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt index 72415f84fe..df1ce9d7f4 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt @@ -1,110 +1,19 @@ -#SETUP: PowerFxV1CompatibilityRules,EnableExpressionChaining,MutationFunctionsTestSetup +#SETUP: PowerFxV1CompatibilityRules,EnableExpressionChaining,StronglyTypedBuiltinEnums,MutationFunctionsTestSetup // Check MutationFunctionsTestSetup handler (PowerFxEvaluationTests.cs) for documentation. ->> Collect(t1, r2);Remove(t1, r1);t1 -Table({Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) - ->> Collect(t1, r1);Collect(t1, r1);Collect(t1, r1);Collect(t1, r2);Remove(t1, r1, "All");t1 -Table({Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) - ->> Collect(t1, r2); - Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:4,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Remove(t1, {Field4:false}); - t1 -Error({Kind:ErrorKind.NotFound}) - ->> Collect(t1, r2); - Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:4,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Remove(t1, {Field4:false}, "All"); - t1 -Error({Kind:ErrorKind.NotFound}) - ->> Collect(t1, r2); - Collect(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Remove(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:true}, "All"); - t1 -Error({Kind:ErrorKind.NotFound}) - ->> Collect(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:1/0,Field2:"moon",Field3:DateTime(2030,2,1,0,0,0,0),Field4:true}); - Remove(t1, {Field2:"earth"}, {Field2:"moon"}, "All"); - t1 -Error(Table({Kind:ErrorKind.NotFound},{Kind:ErrorKind.NotFound})) - ->> Collect(t1, r2); - Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:4,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Remove(t1, {DisplayNameField1:3,DisplayNameField2:"earth",DisplayNameField3:DateTime(2022,2,1,0,0,0,0),DisplayNameField4:false}); - t1 -Table({Field1:1,Field2:"earth",Field3:DateTime(2022,1,1,0,0,0,0),Field4:true},{Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false},{Field1:4,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) - ->> Collect(t1, r2); - Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); - Remove(t1, {DisplayNameField1:3,DisplayNameField2:"earth",DisplayNameField3:DateTime(2022,2,1,0,0,0,0),DisplayNameField4:false}, "All"); - t1 -Table({Field1:1,Field2:"earth",Field3:DateTime(2022,1,1,0,0,0,0),Field4:true},{Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) - ->> Remove(t2, {Field1:1,Field2:{Field2_1:121,Field2_2:"2_2",Field2_3:{Field2_3_1:1231,Field2_3_2:"common"}},Field3:false}); t2 -Table({Field1:2,Field2:{Field2_1:221,Field2_2:"2_2",Field2_3:{Field2_3_1:2231,Field2_3_2:"common"}},Field3:false},{Field1:3,Field2:{Field2_1:321,Field2_2:"2_2",Field2_3:{Field2_3_1:3231,Field2_3_2:"common"}},Field3:true}) - ->> Remove(t2, {Field1:1,Field2:{Field2_1:121,Field2_2:"2_2",Field2_3:{Field2_3_1:1231}},Field3:false}); t2 -Errors: Error 11-98: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-98: Missing column. Your formula is missing a column 'DisplayNameField2_3.DisplayNameField2_3_2' with a type of 'Text'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field1:2,Field2:{Field2_1:221,Field2_3:{Field2_3_1:2231,Field2_3_2:"common"}},Field3:false}); t2 -Errors: Error 11-103: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-103: Missing column. Your formula is missing a column 'DisplayNameField2_2' with a type of 'Text'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field2:{Field2_1:321, Field2_2:"Moon"}}); t2 -Errors: Error 11-51: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-51: Missing column. Your formula is missing a column 'DisplayNameField2_3' with a type of 'Record'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field2:{Field2_3:{Field2_3_1:3231}}}) ; t2 -Errors: Error 11-48: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-48: Missing column. Your formula is missing a column 'DisplayNameField2_1' with a type of 'Decimal'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field2:{Field2_1:321,Field2_2:"2_2",Field2_3:{Field2_3_1:3231,Field2_3_2:"common"}}}); t2 -Error({Kind:ErrorKind.NotFound}) - ->> Remove(t2, {Field1:5}) -Error({Kind:ErrorKind.NotFound}) - ->> Remove(t2, {Field2:{Field2_1:555,Field2_2:"2_2",Field2_3:{Field2_3_1:3231,Field2_3_2:"common"}}}); -Error({Kind:ErrorKind.NotFound}) - ->> Remove(t2, {Field2:{Field2_1:321,Field2_2:"2_2",Field2_3:{Field2_3_1:555,Field2_3_2:"common"}}}); -Error({Kind:ErrorKind.NotFound}) - - ->> Remove(t2, {Field2:{Field2_3:{Field2_3_2:"common"}}}, "All") -Errors: Error 11-52: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-52: Missing column. Your formula is missing a column 'DisplayNameField2_1' with a type of 'Decimal'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field1:5}, "All") -Error({Kind:ErrorKind.NotFound}) - ->> Remove(t2, {Field2:{Field2_1:321}});t2 -Errors: Error 11-34: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-34: Missing column. Your formula is missing a column 'DisplayNameField2_2' with a type of 'Text'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field2:{Field2_3:{Field2_3_1:3231}}});t2 -Errors: Error 11-48: Invalid argument type. Expecting a Record value, but of a different schema.|Error 11-48: Missing column. Your formula is missing a column 'DisplayNameField2_1' with a type of 'Decimal'.|Error 0-6: The function 'Remove' has some invalid arguments. - ->> Remove(t2, {Field2:{Field2_1:321,Field2_2:"2_2",Field2_3:{Field2_3_1:5555,Field2_3_2:"common"}}}, "All");t2 -Error({Kind:ErrorKind.NotFound}) - // Wrong arguments >> Remove(t1, r1,"Al"); -Errors: Error 14-18: If provided, last argument must be 'RemoveFlags.All'. Is there a typo?|Error 0-6: The function 'Remove' has some invalid arguments. +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 14-18: Cannot use a non-record value in this context: '"Al"'. >> Remove(t1, r1,""); -Errors: Error 14-16: If provided, last argument must be 'RemoveFlags.All'. Is there a typo?|Error 0-6: The function 'Remove' has some invalid arguments. +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 14-16: Cannot use a non-record value in this context: '""'. >> Remove(t1, r1, r1, r1, r1, r1, r1, "Al"); -Errors: Error 35-39: If provided, last argument must be 'RemoveFlags.All'. Is there a typo?|Error 0-6: The function 'Remove' has some invalid arguments. +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 35-39: Cannot use a non-record value in this context: '"Al"'. >> Remove(t1, "All"); -Errors: Error 11-16: Invalid argument type (Text). Expecting a Record value instead.|Error 11-16: Cannot use a non-record value in this context.|Error 0-6: The function 'Remove' has some invalid arguments. +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 11-16: Cannot use a non-record value in this context: '"All"'. >> Collect(t1, r2); Collect(t1, {Field1:3,Field2:"earth",Field3:DateTime(2030,2,1,0,0,0,0),Field4:true}); @@ -127,15 +36,9 @@ Table({Field1:1,Field2:"earth",Field3:DateTime(2022,1,1,0,0,0,0),Field4:true},{F t1 Table({Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false},{Field1:3,Field2:"earth",Field3:DateTime(2030,2,1,0,0,0,0),Field4:true},{Field1:4,Field2:"earth",Field3:DateTime(2040,2,1,0,0,0,0),Field4:false}) ->> Remove(Foo, {Field1:5}, "All") -Errors: Error 7-10: Name isn't valid. 'Foo' isn't recognized.|Error 12-22: The specified column 'Field1' does not exist.|Error 0-6: The function 'Remove' has some invalid arguments. +>> Remove(Foo, {Field1:5}, RemoveFlags.All) +Errors: Error 7-10: Name isn't valid. 'Foo' isn't recognized. >> Remove(Foo, Bar) Errors: Error 7-10: Name isn't valid. 'Foo' isn't recognized.|Error 12-15: Name isn't valid. 'Bar' isn't recognized.|Error 0-6: The function 'Remove' has some invalid arguments. ->> Remove(t1, {Field1:2,Field2:"not in the table",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) -Error({Kind:ErrorKind.NotFound}) - -// Remove propagates error. ->> Remove(t1, If(1/0<2, {Field1:2})) -Error({Kind:ErrorKind.Div0}) \ No newline at end of file diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove_V1Compact.txt b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove_V1Compact.txt index b9a95d1f57..c9c871faa2 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove_V1Compact.txt +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove_V1Compact.txt @@ -1,9 +1,8 @@ -#SETUP: PowerFxV1CompatibilityRules -#SETUP: EnableExpressionChaining,MutationFunctionsTestSetup +#SETUP: PowerFxV1CompatibilityRules,EnableExpressionChaining,MutationFunctionsTestSetup,StronglyTypedBuiltinEnums // Check MutationFunctionsTestSetup handler (PowerFxEvaluationTests.cs) for documentation. ->> Remove(t2, {Field1:1,Field2:{Field2_1:121,Field2_2:"2_2",Field2_3:{Field2_3_1:1231,Field2_3_2:"common"}},Field3:false}, "All") +>> Remove(t2, {Field1:1,Field2:{Field2_1:121,Field2_2:"2_2",Field2_3:{Field2_3_1:1231,Field2_3_2:"common"}},Field3:false}, RemoveFlags.All) If(true, {test:1}, "Void value (result of the expression can't be used).") >> Collect(t1, {Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}); diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/AndOr_V1Compat.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/AndOr_V1Compat.txt index b7ac223105..fafc0746e7 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/AndOr_V1Compat.txt +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/AndOr_V1Compat.txt @@ -1,4 +1,4 @@ -#SETUP: PowerFxV1CompatibilityRules +#SETUP: PowerFxV1CompatibilityRules,StronglyTypedBuiltinEnums // Case to test how shortcut verification work along with behavior functions @@ -98,7 +98,7 @@ true Table({Value:false},{Value:true}) // replaced with if from _V1CompatDisabled since short circuiting not supported with Void ->> If( !true, Remove(t1, First(t1), "All")); t1 // !true || operator +>> If( !true, Remove(t1, First(t1), RemoveFlags.All)); t1 // !true || operator Table({Value:false},{Value:true}) >> -3;t1 @@ -126,7 +126,7 @@ true Table({Value:true},{Value:true},{Value:true}) // replaced with if from _V1CompatDisabled since short circuiting not supported with Void ->> If( !false, Remove(t1, First(t1), "All")); t1 // || Operator +>> If( !false, Remove(t1, First(t1), RemoveFlags.All)); t1 // || Operator Table() >> 3;t1 @@ -152,5 +152,5 @@ true Table({Value:true},{Value:true},{Value:true}) // replaced with if from _V1CompatDisabled since short circuiting not supported with Void ->> If( !false, Remove(t1, First(t1), "All")); t1 // Or Function +>> If( !false, Remove(t1, First(t1), RemoveFlags.All)); t1 // Or Function Table() diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove.txt new file mode 100644 index 0000000000..7d7965b101 --- /dev/null +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove.txt @@ -0,0 +1,108 @@ +#SETUP: PowerFxV1CompatibilityRules,StronglyTypedBuiltinEnums + +>> Set(t1, Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)})) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Set(t2, t1) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t1, First(t1)) +If(true, {test:1}, "Void value (result of the expression can't be used).") + +>> 0;t1 +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> 0;Set(t1, t2) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t1, First(t1), RemoveFlags.All) +If(true, {test:1}, "Void value (result of the expression can't be used).") + +>> 1;t1 +Table({a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> 1;Set(t1, t2) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t1, {a:true}) +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 11-19: Missing column. Your formula is missing a column 'b' with a type of 'Text'. + +>> Remove(t1, {a:true,b:true}) +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 11-26: Incompatible type. The 'b' column in the data source you’re updating expects a 'Text' type and you’re using a 'Boolean' type. + +>> 2;t1 +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t1, {a:true,b:"does not exist",c:Now()}) +Error({Kind:ErrorKind.NotFound}) + +>> Remove(t1, {a:true,b:"does not exist",c:Now()}, RemoveFlags.All) +Error({Kind:ErrorKind.NotFound}) + +>> Remove(t1, {a:true,b:"does not exist",c:Now()}, {a:false,b:"does not exist",c:Now()}, RemoveFlags.All) +Error(Table({Kind:ErrorKind.NotFound},{Kind:ErrorKind.NotFound})) + +>> 3;t1 +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t1, If(1/0<2, {a:true,b:"hello"})) +Errors: Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 11-40: Missing column. Your formula is missing a column 'c' with a type of 'DateTime'. + +>> 4;t1 +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Set(t3, t1) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +// Removing multiple rows with the same values. +>> Remove(t3, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}) +If(true, {test:1}, "Void value (result of the expression can't be used).") + +>> 0;t3 +Table({a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> 0;Set(t3, t1) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t3, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)}, RemoveFlags.All) +Error(Table({Kind:ErrorKind.NotFound},{Kind:ErrorKind.NotFound})) + +>> 1;t3 +Table({a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> 1;Set(t3, t1) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t3, t3) +If(true, {test:1}, "Void value (result of the expression can't be used).") + +>> 2;t3 +Table() + +>> 2;Set(t3, t1) +Table({a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:true,b:"hi",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hello",c:DateTime(2024,1,1,0,0,0,0)},{a:false,b:"hi",c:DateTime(2024,1,1,0,0,0,0)}) + +>> Remove(t3, t3, RemoveFlags.All) +Error(Table({Kind:ErrorKind.NotFound},{Kind:ErrorKind.NotFound})) + +// Remove propagates error. +>> Remove(t1, If(1/0<2, {a:true,b:"hello",c:DateTime(2024,1,1,0,0,0,0)})) +Error({Kind:ErrorKind.Div0}) + +>> Set(t4, Table({a:{aa:{aaa:true,bbb:true}}})) +Table({a:{aa:{aaa:true,bbb:true}}}) + +>> Remove(t4, {a:{aa:{aaa:true}}}) +Errors: Error 0-6: The function 'Remove' has some invalid arguments.|Error 11-30: Missing column. Your formula is missing a column 'a.aa.bbb' with a type of 'Boolean'. + +>> Remove(t4, {a:{aa:{aaa:true,bbb:false}}}) +Error({Kind:ErrorKind.NotFound}) + +>> Remove(t4, {a:{aa:{aaa:true,bbb:false}}}, RemoveFlags.All) +Error({Kind:ErrorKind.NotFound}) + +>> Remove(t4, {a:{aa:{aaa:true,bbb:true}}}) +If(true, {test:1}, "Void value (result of the expression can't be used).") + +>> t4 +Table() \ No newline at end of file diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1Compat.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1Compat.txt deleted file mode 100644 index 761f0ce599..0000000000 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1Compat.txt +++ /dev/null @@ -1,22 +0,0 @@ -#SETUP: PowerFxV1CompatibilityRules - ->> Set(list, Table({ Name: "One", ID: 1}, { Name: "Two", ID: 2})) -Table({ID:1,Name:"One"},{ID:2,Name:"Two"}) - ->> Set(list2, Table(First(list))) -Table({ID:1,Name:"One"}) - ->> ForAll(list2, Remove(list, ThisRecord)) -If(true, {test:1}, "Void value (result of the expression can't be used).") - ->> list -Table({ID:2,Name:"Two"}) - ->> Set(list3, [1,2,3,4]) -Table({Value:1},{Value:2},{Value:3},{Value:4}) - ->> Remove(list3, {Value:3}) -If(true, {test:1}, "Void value (result of the expression can't be used).") - ->> Remove(list3, {Value:5}) -Error({Kind:ErrorKind.NotFound}) \ No newline at end of file diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt deleted file mode 100644 index 5d8da13dc6..0000000000 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt +++ /dev/null @@ -1,22 +0,0 @@ -#SETUP: disable:PowerFxV1CompatibilityRules - ->> Set(list, Table({ Name: "One", ID: 1}, { Name: "Two", ID: 2})) -Table({ID:1,Name:"One"},{ID:2,Name:"Two"}) - ->> Set(list2, Table(First(list))) -Table({ID:1,Name:"One"}) - ->> ForAll(list2, Remove(list, ThisRecord)) -Table(Blank()) - ->> list -Table({ID:2,Name:"Two"}) - ->> Set(list3, [1,2,3,4]) -Table({Value:1},{Value:2},{Value:3},{Value:4}) - ->> Remove(list3, {Value:3}) -Blank() - ->> Remove(list3, {Value:5}) -Error({Kind:ErrorKind.NotFound}) \ No newline at end of file diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs index 705ca728b7..1dd46080a3 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; +using System.Linq; using Microsoft.PowerFx.Interpreter.Tests; using Microsoft.PowerFx.Types; using Xunit; @@ -164,8 +165,8 @@ public void DataTableEvalTest2() var result7 = engine.Eval("Patch(robintable, First(robintable),{Names:\"new-name\"});robintable", options: opt); Assert.Equal("Table({Names:\"new-name\",Scores:10},{Names:\"name3\",Scores:30},{Names:\"name100\",Scores:10})", ((DataTableValue)result7).Dump()); - var result8 = engine.Eval("Remove(robintable, {Scores:10}, \"All\");robintable", options: opt); - Assert.IsType(result8); + var check = engine.Check("Remove(robintable, {Scores:10}, RemoveFlags.All)", options: opt); + Assert.False(check.IsSuccess); Assert.Equal(3, table.Rows.Count); }