From bf65e7d0dfe9a793963363ba8a23321df2070357 Mon Sep 17 00:00:00 2001 From: anderson-joyle Date: Tue, 10 Sep 2024 18:16:34 -0500 Subject: [PATCH] WIP Remove function --- .../Localization/Strings.cs | 10 +- .../Public/Config/SymbolTable.cs | 9 +- .../Public/Values/CollectionTableValue.cs | 4 +- .../Public/Values/TableValue.cs | 2 +- .../Texl/Builtins/Remove.cs | 626 ++++++++++++++++++ .../Types/Enums/BuiltInEnums.cs | 10 + .../Types/Enums/EnumStoreBuilder.cs | 5 + .../Utils/LanguageConstants.cs | 5 + .../Environment/PowerFxConfigExtensions.cs | 3 +- .../Functions/LibraryMutation.cs | 18 + .../Functions/Mutation/MutationUtils.cs | 91 +++ .../Functions/Mutation/RemoveFunction.cs | 239 ------- src/strings/PowerFxResources.en-US.resx | 94 ++- .../ExpressionTestCases/Remove.txt | 24 +- .../ExpressionTestCases/Remove_V1Compact.txt | 4 +- .../MutationScripts/AndOr_V1Compat.txt | 8 +- .../Remove_V1CompatDisabled.txt | 4 +- .../PADIntegrationTests.cs | 2 +- 18 files changed, 883 insertions(+), 275 deletions(-) create mode 100644 src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Remove.cs delete mode 100644 src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs diff --git a/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs b/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs index 68008bb151..a19ed5851c 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); @@ -649,6 +651,7 @@ internal static class TexlStrings public static ErrorResourceKey ErrBadSchema_ExpectedType = new ErrorResourceKey("ErrBadSchema_ExpectedType"); public static ErrorResourceKey ErrInvalidArgs_Func = new ErrorResourceKey("ErrInvalidArgs_Func"); public static ErrorResourceKey ErrNeedTable_Func = new ErrorResourceKey("ErrNeedTable_Func"); + public static ErrorResourceKey ErrNeedTable_Arg = new ErrorResourceKey("ErrNeedTable_Arg"); public static ErrorResourceKey ErrNeedTableCol_Func = new ErrorResourceKey("ErrNeedTableCol_Func"); public static ErrorResourceKey ErrNotAccessibleInCurrentContext = new ErrorResourceKey("ErrNotAccessibleInCurrentContext"); public static ErrorResourceKey ErrInternalControlInInputProperty = new ErrorResourceKey("ErrInternalControlInInputProperty"); @@ -843,5 +846,8 @@ internal static class TexlStrings public static ErrorResourceKey ErrInvalidDataSourceForFunction = new ErrorResourceKey("ErrInvalidDataSourceForFunction"); public static ErrorResourceKey ErrInvalidArgumentExpectedType = new ErrorResourceKey("ErrInvalidArgumentExpectedType"); public static ErrorResourceKey ErrUnsupportedTypeInTypeArgument = new ErrorResourceKey("ErrUnsupportedTypeInTypeArgument"); + 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/SymbolTable.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs index 86b1d4e6ea..ae9e300bf0 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs @@ -348,10 +348,15 @@ internal void AddFunction(TexlFunction function) { using var guard = _guard.Enter(); // Region is single threaded. Inc(); - _functions.Add(function); + _functions.Add(function); + + if (EnumStoreBuilder == null) + { + _enumStoreBuilder = new EnumStoreBuilder(); + } // Add any associated enums - EnumStoreBuilder?.WithRequiredEnums(new TexlFunctionSet(function)); + EnumStoreBuilder.WithRequiredEnums(new TexlFunctionSet(function)); } internal EnumStoreBuilder EnumStoreBuilder diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs index fee7d280c7..6f9387fd8a 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CollectionTableValue.cs @@ -220,7 +220,7 @@ public override async Task> RemoveAsync(IEnumerable> PatchCoreAsync(RecordValue ba /// Should we make a copy of the found record, ahead of mutation./// /// A record instance within the current table. This record can then be updated. /// A derived class may override if there's a more efficient way to find the match than by linear scan. - protected virtual async Task FindAsync(RecordValue baseRecord, CancellationToken cancellationToken, bool mutationCopy = false) + internal virtual async Task FindAsync(RecordValue baseRecord, CancellationToken cancellationToken, bool mutationCopy = false) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Values/TableValue.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Values/TableValue.cs index 80383fa73d..2f24165c83 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Values/TableValue.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Values/TableValue.cs @@ -234,7 +234,7 @@ public virtual async Task> ClearAsync(CancellationToken can { return DValue.Of(NotImplementedError(IRContext)); } - + /// /// Patch implementation for derived classes. /// diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Remove.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Remove.cs new file mode 100644 index 0000000000..f8e214295e --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Remove.cs @@ -0,0 +1,626 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.PowerFx.Core.App.ErrorContainers; +using Microsoft.PowerFx.Core.Binding; +using Microsoft.PowerFx.Core.Entities; +using Microsoft.PowerFx.Core.Entities.QueryOptions; +using Microsoft.PowerFx.Core.Errors; +using Microsoft.PowerFx.Core.Functions; +using Microsoft.PowerFx.Core.Functions.DLP; +using Microsoft.PowerFx.Core.Localization; +using Microsoft.PowerFx.Core.Logging.Trackers; +using Microsoft.PowerFx.Core.Types; +using Microsoft.PowerFx.Core.Types.Enums; +using Microsoft.PowerFx.Core.Utils; +using Microsoft.PowerFx.Syntax; + +namespace Microsoft.PowerFx.Core.Texl.Builtins +{ + // Remove(collection:*[], item1:![], item2:![], ..., ["All"]) + + // !!!TODO MOVE THIS TO src/Cloud/DocumentServer.Core/Document/Texl/Functions/Libraries/BuiltinFunctions.cs + //[RequiresErrorContext] + internal class RemoveFunction : BuiltinFunction, ISuggestionAwareFunction + { + public override bool ManipulatesCollections => true; + + public override bool ModifiesValues => true; + + public override bool CanSuggestInputColumns => true; + + public override bool IsSelfContained => false; + + public bool CanSuggestThisItem => true; + + public override bool RequiresDataSourceScope => true; + + public override bool SupportsParamCoercion => false; + + public override RequiredDataSourcePermissions FunctionPermission => RequiredDataSourcePermissions.Delete; + + // Return true if this function affects datasource query options. + public override bool AffectsDataSourceQueryOptions => true; + + public override bool MutatesArg(int argIndex, TexlNode arg) => argIndex == 0; + + public override bool ArgMatchesDatasourceType(int argNum) + { + return argNum >= 1; + } + + public override bool TryGetTypeForArgSuggestionAt(int argIndex, out DType type) + { + if (argIndex > 0) + { + type = default; + return false; + } + + return base.TryGetTypeForArgSuggestionAt(argIndex, out type); + } + + public RemoveFunction() + : base("Remove", TexlStrings.AboutRemove, FunctionCategories.Behavior, DType.EmptyTable, 0, 2, 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 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 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) + { + if (context.AnalysisMode) + { + if (!DType.String.Accepts(argType, exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules) && + !BuiltInEnums.RemoveFlagsEnum.FormulaType._type.Accepts(argTypes[i], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) + { + fValid = false; + errors.EnsureError(DocumentErrorSeverity.Severe, args[i], TexlStrings.ErrRemoveAllArg); + } + } + else + { + if (!BuiltInEnums.RemoveFlagsEnum.FormulaType._type.Accepts(argTypes[i], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) + { + fValid = false; + errors.EnsureError(DocumentErrorSeverity.Severe, args[i], TexlStrings.ErrRemoveAllArg); + } + } + + continue; + } + else + { + 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); + + // The item schema should be compatible with the collection schema. + if (!collectionAcceptsRecord && !recordAcceptsCollection) + { + fValid = false; + if (!SetErrorForMismatchedColumns(collectionType, argType, args[i], errors, context.Features)) + { + errors.EnsureError(DocumentErrorSeverity.Severe, args[i], TexlStrings.ErrCollectionDoesNotAcceptThisType); + } + } + + // 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); + } + + if (!context.AnalysisMode) + { + // ArgType[N] (0= 0); + + return argumentIndex != 1; + } + + 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; + } + + /// + /// 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; + } + + public override bool IsAsyncInvocation(CallNode callNode, TexlBinding binding) + { + Contracts.AssertValue(callNode); + Contracts.AssertValue(binding); + + return Arg0RequiresAsync(callNode, binding); + } + + // !!!TODO MOVE THIS TO + // src/Cloud/DocumentServer.Core/Document/Texl/Functions/Libraries/BuiltinFunctions.cs and + // src/Cloud/DocumentServer.Core/Document/Publish/TryPushCustomJsExpressionHandlers.cs + //public static void PushCustomJsArgs(TexlFunction func, JsTranslator translator, TexlBinding binding, CallNode node, List argFragments) + //{ + // Contracts.Assert(argFragments.Count >= 1); + + // // If the "ALL" arg was not specified, inject an empty string arg instead. + // var args = node.Args.Children; + + // if (argFragments.Count < 3 || !DType.String.Accepts(binding.GetType(args[argFragments.Count - 1]), exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: binding.Features.PowerFxV1CompatibilityRules)) + // { + // argFragments.Add(translator.CreateFragment(PAStringBuilderConst.EmptyQuotes)); + // } + //} + + 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"]) + // !!!TODO MOVE THIS TO src/Cloud/DocumentServer.Core/Document/Texl/Functions/Libraries/BuiltinFunctions.cs + //[RequiresErrorContext] + //[TexlRuntimeNameOverride(Suffix = "All")] + internal class RemoveAllFunction : BuiltinFunction + { + public override bool ManipulatesCollections => true; + + public override bool ModifiesValues => 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 ArgMatchesDatasourceType(int argNum) + { + return argNum == 1; + } + + public RemoveAllFunction() + : base("Remove", TexlStrings.AboutRemove, FunctionCategories.Behavior, DType.EmptyTable, 0, 2, 3, DType.EmptyTable, DType.EmptyTable, DType.String) + { + } + + 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 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 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]); + } + + if (!context.AnalysisMode) + { + bool checkAggregateNames = sourceType.CheckAggregateNames(collectionType, args[1], errors, context.Features, SupportsParamCoercion); + + // The item schema should be compatible with the collection schema. + if (!checkAggregateNames) + { + fValid = false; + if (!SetErrorForMismatchedColumns(collectionType, sourceType, args[1], errors, context.Features)) + { + errors.EnsureError(DocumentErrorSeverity.Severe, args[1], TexlStrings.ErrTableDoesNotAcceptThisType); + } + } + } + + // The source schema should be compatible with the collection schema. + else 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) + { + if (context.AnalysisMode) + { + if (!DType.String.Accepts(argTypes[2], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules) && + !BuiltInEnums.RemoveFlagsEnum.FormulaType._type.Accepts(argTypes[2], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) + { + fValid = false; + errors.EnsureError(DocumentErrorSeverity.Severe, args[2], TexlStrings.ErrRemoveAllArg); + } + } + else + { + if (!BuiltInEnums.RemoveFlagsEnum.FormulaType._type.Accepts(argTypes[2], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules)) + { + fValid = false; + errors.EnsureError(DocumentErrorSeverity.Severe, args[2], TexlStrings.ErrRemoveAllArg); + } + } + } + + if (context.AnalysisMode) + { + // Remove returns the new collection, so the return schema is the same as the collection schema. + returnType = collectionType; + } + else + { + returnType = DType.Void; + } + + 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); + MutationUtils.CheckSemantics(binding, this, args, argTypes, errors); + } + + // 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 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); + } + + // !!!TODO MOVE THIS TO + // src/Cloud/DocumentServer.Core/Document/Texl/Functions/Libraries/BuiltinFunctions.cs and + // src/Cloud/DocumentServer.Core/Document/Publish/TryPushCustomJsExpressionHandlers.cs + //public static void PushCustomJsArgs(TexlFunction func, JsTranslator translator, TexlBinding binding, CallNode node, List argFragments) + //{ + // Contracts.Assert(argFragments.Count >= 1); + + // if (func.IsServerDelegatable(node, binding)) + // { + // // If remove all is delegatable && the "ALL" arg was not specified, the translator handles it + // return; + // } + + // // If the "ALL" arg was not specified, inject an empty string arg instead. + // var args = node.Args.Children; + // if (argFragments.Count < 3 || !DType.String.Accepts(binding.GetType(args[argFragments.Count - 1]), exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: binding.Features.PowerFxV1CompatibilityRules)) + // { + // argFragments.Add(translator.CreateFragment(PAStringBuilderConst.EmptyQuotes)); + // } + //} + + public override bool IsServerDelegatable(CallNode callNode, TexlBinding binding) + { + Contracts.AssertValue(callNode); + Contracts.AssertValue(binding); + + if (!CheckArgsCount(callNode, binding)) + { + return false; + } + + // !!!TODO Check with PA team + // Use ECS flag as a guard. + //if (binding.Document != null) + //{ + // if (binding.Document.Properties is DocumentProperties documentProperties) + // { + // if (!documentProperties.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 1974d0dedf..cdf0b90ba0 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/BuiltInEnums.cs @@ -199,5 +199,15 @@ private static Dictionary TraceSeverityDictionary() { "IgnoreUnsupportedTypes", TraceFunction.IgnoreUnsupportedTypesEnumValue }, }, canConcatenateStronglyTyped: true); + + public static readonly EnumSymbol RemoveFlagsEnum = new EnumSymbol( + new DName(LanguageConstants.RemoveFlagsEnumString), + DType.String, + new Dictionary() + { + { "First", "first" }, + { "All", "all" }, + }, + canCoerceFromBackingKind: true); } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs index ff4403bc2e..3a849d9fb8 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Types/Enums/EnumStoreBuilder.cs @@ -30,6 +30,7 @@ internal sealed class EnumStoreBuilder { LanguageConstants.JSONFormatEnumString, BuiltInEnums.JSONFormatEnum }, { LanguageConstants.TraceSeverityEnumString, BuiltInEnums.TraceSeverityEnum }, { LanguageConstants.TraceOptionsEnumString, BuiltInEnums.TraceOptionsEnum }, + { LanguageConstants.RemoveFlagsEnumString, BuiltInEnums.RemoveFlagsEnum }, }; // DefaultEnums, with enum strings, is legacy and only used by Power Apps @@ -79,6 +80,10 @@ internal sealed class EnumStoreBuilder { LanguageConstants.TraceOptionsEnumString, $"%s[{string.Join(", ", BuiltInEnums.TraceOptionsEnum.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 cfeb1ce077..e6efc8b05d 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Utils/LanguageConstants.cs @@ -75,5 +75,10 @@ internal class LanguageConstants /// The string value representing the JSON format. /// internal const string JSONFormatEnumString = "JSONFormat"; + + /// + /// 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 ebdaa102c5..201615ea6b 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Environment/PowerFxConfigExtensions.cs @@ -42,7 +42,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 bbb726a7ca..cc8c2599d5 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryMutation.cs @@ -402,4 +402,22 @@ public async Task InvokeAsync(FormulaType irContext, FormulaValue[ return await new ClearCollectImpl().InvokeAsync(irContext, args, cancellationToken).ConfigureAwait(false); } } + + // Remove(collection:*[], item1:![], item2:![], ..., ["All"]) + internal class RemoveImpl : RemoveFunction, IAsyncTexlFunction3 + { + public async Task InvokeAsync(FormulaType irContext, FormulaValue[] args, CancellationToken cancellationToken) + { + return await MutationUtils.RemoveCore(irContext, args, cancellationToken).ConfigureAwait(false); + } + } + + // Remove(collection:*[], source:*[], ["All"]) + internal class RemoveAllImpl : RemoveAllFunction, IAsyncTexlFunction3 + { + public async Task InvokeAsync(FormulaType irContext, FormulaValue[] args, CancellationToken cancellationToken) + { + return await MutationUtils.RemoveCore(irContext, args, 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..57dab31ff0 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/MutationUtils.cs @@ -1,13 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.PowerFx.Core.App.ErrorContainers; using Microsoft.PowerFx.Core.Entities; using Microsoft.PowerFx.Core.Errors; +using Microsoft.PowerFx.Core.IR; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; +using Microsoft.PowerFx.Core.Types.Enums; +using Microsoft.PowerFx.Functions; +using Microsoft.PowerFx.Interpreter.Localization; using Microsoft.PowerFx.Syntax; using Microsoft.PowerFx.Types; @@ -47,5 +54,89 @@ 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(FormulaType irContext, FormulaValue[] args, CancellationToken cancellationToken) + { + 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) are an 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) + .Select(row => (RecordValue)row) + .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(IRContext.NotInSource(irContext), errors); + } + + return irContext == 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 e0bb724273..0000000000 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs +++ /dev/null @@ -1,239 +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(FormulaValue[] 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, IAsyncTexlFunction3 - { - 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(FormulaType irContextRet, FormulaValue[] args, CancellationToken cancellationToken) - { - 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.Length - 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, irContextRet == FormulaType.Void ? FormulaType.Void : FormulaType.Blank); - } - else - { - result = irContextRet == 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 3f818dcd1a..d739637b29 100644 --- a/src/strings/PowerFxResources.en-US.resx +++ b/src/strings/PowerFxResources.en-US.resx @@ -1997,6 +1997,10 @@ Property expects a required parameter. Please use parentheses to pass the required parameter. Error Message. + + Cannot use a non-table value in this context: '{0}'. + Error Message. + The first argument of '{0}' should be a one-column table. Error Message. @@ -3290,16 +3294,36 @@ 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. + + + item + function_parameter - Second parameter to the Remove function - the item to be removed. + + + The collection to remove rows from. - - 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). + + 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. If provided, last argument must be 'RemoveFlags.All'. Is there a typo? @@ -4744,6 +4768,10 @@ The source to clear. + + The first argument of '{0}' should be a collection. + Error Message. + The specified data source cannot be used with this function. Error Message. @@ -4756,4 +4784,56 @@ Unsupported untyped/JSON conversion type '{0}' in argument. Error Message shown to user when a unsupported type is passed in type argument of AsType, IsType and ParseJSON functions. + + 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..c295bec9a5 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/Remove.txt @@ -1,11 +1,11 @@ -#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 +>> Collect(t1, r1);Collect(t1, r1);Collect(t1, r1);Collect(t1, r2);Remove(t1, r1, RemoveFlags.All);t1 Table({Field1:2,Field2:"moon",Field3:DateTime(2022,2,1,0,0,0,0),Field4:false}) >> Collect(t1, r2); @@ -18,21 +18,21 @@ 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"); + Remove(t1, {Field4:false}, RemoveFlags.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"); + Remove(t1, {Field1:1/0,Field2:"earth",Field3:DateTime(2022,2,1,0,0,0,0),Field4:true}, RemoveFlags.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"); + Remove(t1, {Field2:"earth"}, {Field2:"moon"}, RemoveFlags.All); t1 Error(Table({Kind:ErrorKind.NotFound},{Kind:ErrorKind.NotFound})) @@ -46,7 +46,7 @@ Table({Field1:1,Field2:"earth",Field3:DateTime(2022,1,1,0,0,0,0),Field4:true},{F >> 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"); + Remove(t1, {DisplayNameField1:3,DisplayNameField2:"earth",DisplayNameField3:DateTime(2022,2,1,0,0,0,0),DisplayNameField4:false}, RemoveFlags.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}) @@ -81,7 +81,7 @@ 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") +>> Remove(t2, {Field1:5}, RemoveFlags.All) Error({Kind:ErrorKind.NotFound}) >> Remove(t2, {Field2:{Field2_1:321}});t2 @@ -90,7 +90,7 @@ Errors: Error 11-34: Invalid argument type. Expecting a Record value, but of a d >> 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 +>> Remove(t2, {Field2:{Field2_1:321,Field2_2:"2_2",Field2_3:{Field2_3_1:5555,Field2_3_2:"common"}}}, RemoveFlags.All);t2 Error({Kind:ErrorKind.NotFound}) // Wrong arguments @@ -101,10 +101,10 @@ Errors: Error 14-18: If provided, last argument must be 'RemoveFlags.All'. Is th 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. >> 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: If provided, last argument must be 'RemoveFlags.All'. Is there a typo? >> 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,8 +127,8 @@ 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.|Warning 12-22: Incompatible type. The collection can't contain values of this type.|Error 0-6: The function 'Remove' has some invalid arguments.|Error 12-22: The specified column 'Field1' does not exist. >> 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. 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..c4fec9328d 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,9 @@ #SETUP: PowerFxV1CompatibilityRules -#SETUP: EnableExpressionChaining,MutationFunctionsTestSetup +#SETUP: 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_V1CompatDisabled.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt index 5d8da13dc6..d1e6d8cdb2 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/Remove_V1CompatDisabled.txt @@ -7,7 +7,7 @@ Table({ID:1,Name:"One"},{ID:2,Name:"Two"}) Table({ID:1,Name:"One"}) >> ForAll(list2, Remove(list, ThisRecord)) -Table(Blank()) +If(true, {test:1}, "Void value (result of the expression can't be used).") >> list Table({ID:2,Name:"Two"}) @@ -16,7 +16,7 @@ Table({ID:2,Name:"Two"}) Table({Value:1},{Value:2},{Value:3},{Value:4}) >> Remove(list3, {Value:3}) -Blank() +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/PADIntegrationTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs index 705ca728b7..fd37de852e 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/PADIntegrationTests.cs @@ -164,7 +164,7 @@ 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); + var result8 = engine.Eval("Remove(robintable, {Scores:10}, RemoveFlags.All);robintable", options: opt); Assert.IsType(result8); Assert.Equal(3, table.Rows.Count);