Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,11 @@ internal static class TexlStrings
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 AboutUpdate = (b) => StringResources.Get("AboutUpdate", b);
public static StringGetter UpdateDataSourceArg = (b) => StringResources.Get("UpdateDataSourceArg", b);
public static StringGetter UpdateOldRecordArg = (b) => StringResources.Get("UpdateOldRecordArg", b);
public static StringGetter UpdateNewRecordArg = (b) => StringResources.Get("UpdateNewRecordArg", b);
public static StringGetter UpdateAllArg = (b) => StringResources.Get("UpdateAllArg", b);

public static StringGetter AboutDec2Hex = (b) => StringResources.Get("AboutDec2Hex", b);
public static StringGetter Dec2HexArg1 = (b) => StringResources.Get("Dec2HexArg1", b);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,52 @@ protected override async Task<DValue<RecordValue>> PatchCoreAsync(RecordValue ba
}
}

internal override async Task<DValue<BooleanValue>> UpdateAsync(RecordValue oldRecord, RecordValue replacementRecord, bool all, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (_sourceMutableIndex == null)
{
return await base.UpdateAsync(oldRecord, replacementRecord, all, cancellationToken).ConfigureAwait(false);
}

var found = false;

for (var index = 0; index < _sourceMutableIndex.Count; index++)
{
cancellationToken.ThrowIfCancellationRequested();

var current = Marshal(_sourceMutableIndex[index]);
if (current.IsError)
{
return DValue<BooleanValue>.Of(current.Error);
}

if (current.IsBlank)
{
continue;
}

if (await MatchesAsync(current.Value, oldRecord, cancellationToken).ConfigureAwait(false))
{
found = true;
_sourceMutableIndex[index] = MarshalInverse((RecordValue)replacementRecord.MaybeShallowCopy());

if (!all)
{
break;
}
}
}

if (!found)
{
return DValue<BooleanValue>.Of(FormulaValue.NewError(new ExpressionError() { Message = "The specified record was not found.", Kind = ErrorKind.NotFound }));
}

return DValue<BooleanValue>.Of(New(true));
}

/// <summary>
/// Execute a linear search for the matching record.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ public virtual async Task<DValue<BooleanValue>> ClearAsync(CancellationToken can
return DValue<BooleanValue>.Of(NotImplementedError(IRContext));
}

internal virtual async Task<DValue<BooleanValue>> UpdateAsync(RecordValue oldRecord, RecordValue replacementRecord, bool all, CancellationToken cancellationToken)
{
var result = await PatchAsync(oldRecord, replacementRecord, cancellationToken).ConfigureAwait(false);

if (result.IsError)
{
return DValue<BooleanValue>.Of(result.Error);
}

return DValue<BooleanValue>.Of(FormulaValue.New(true));
}

/// <summary>
/// Patch implementation for derived classes.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static void EnableMutationFunctions(this SymbolTable symbolTable)
symbolTable.AddFunction(new PatchAggregateImpl());
symbolTable.AddFunction(new PatchAggregateSingleTableImpl());
symbolTable.AddFunction(new RemoveFunction());
symbolTable.AddFunction(new UpdateFunction());
symbolTable.AddFunction(new ClearImpl());
symbolTable.AddFunction(new ClearCollectImpl());
symbolTable.AddFunction(new ClearCollectScalarImpl());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
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.Functions.DLP;
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;

namespace Microsoft.PowerFx.Functions
{
internal class UpdateFunction : RemoveFunctionBase, IFunctionInvoker
{
public override bool SupportsParamCoercion => true;

public override RequiredDataSourcePermissions FunctionPermission => RequiredDataSourcePermissions.Update;

public UpdateFunction()
: base("Update", AboutUpdate, FunctionCategories.Table | FunctionCategories.Behavior, DType.Unknown, 0, 3, 4, DType.EmptyTable, DType.EmptyRecord, DType.EmptyRecord)
{
}

public override IEnumerable<StringGetter[]> GetSignatures()
{
yield return new[] { UpdateDataSourceArg, UpdateOldRecordArg, UpdateNewRecordArg };
yield return new[] { UpdateDataSourceArg, UpdateOldRecordArg, UpdateNewRecordArg, UpdateAllArg };
}

public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DType[] argTypes, IErrorContainer errors, out DType returnType, out Dictionary<TexlNode, DType> 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);
var collectionType = argTypes[0];

if (!collectionType.IsTable)
{
errors.EnsureError(args[0], ErrNeedTable_Func, Name);
fValid = false;
}

for (var i = 1; i <= 2; i++)
{
var argType = argTypes[i];
if (!argType.IsRecord)
{
fValid = false;
errors.EnsureError(args[i], ErrNeedRecord, args[i]);
continue;
}

if (!argType.CheckAggregateNames(collectionType, args[i], errors, context.Features, SupportsParamCoercion))
{
fValid = false;
if (!SetErrorForMismatchedColumns(collectionType, argType, args[i], errors, context.Features))
{
errors.EnsureError(DocumentErrorSeverity.Severe, args[i], ErrTableDoesNotAcceptThisType);
}
}
else if (SupportsParamCoercion && !collectionType.Accepts(argType.ToTable(), exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules))
{
if (argType.TryGetCoercionSubType(collectionType.ToRecord(), out DType coercionType, out bool coercionNeeded, context.Features) && coercionNeeded)
{
CollectionUtils.Add(ref nodeToCoercedTypeMap, args[i], coercionType);
}
}
}

if (args.Length == 4)
{
if (!DType.String.Accepts(argTypes[3], exact: true, useLegacyDateTimeAccepts: false, usePowerFxV1CompatibilityRules: context.Features.PowerFxV1CompatibilityRules) ||
args[3] is not StrLitNode strNode ||
strNode.Value.ToUpperInvariant() != "ALL")
{
fValid = false;
errors.EnsureError(args[3], ErrRemoveAllArg, args[3]);
}
}

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<FormulaValue> 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 arg0 = args[0];
if (arg0 is LambdaFormulaValue arg0lazy)
{
arg0 = await arg0lazy.EvalAsync().ConfigureAwait(false);
}

if (arg0 is BlankValue)
{
return arg0;
}

if (arg0 is not TableValue tableValue)
{
return arg0;
}

if (args[1] is not RecordValue oldRecord)
{
return args[1];
}

if (args[2] is not RecordValue newRecord)
{
return args[2];
}

var updateAll = args.Count == 4 && args[3] is StringValue stringValue && stringValue.Value.ToUpperInvariant() == "ALL";
var replacementRecord = await BuildReplacementRecordAsync(tableValue, newRecord, cancellationToken).ConfigureAwait(false);
var result = await tableValue.UpdateAsync(oldRecord, replacementRecord, updateAll, cancellationToken).ConfigureAwait(false);

FormulaValue output;
if (result.IsError)
{
output = FormulaValue.NewError(result.Error.Errors, returnType == FormulaType.Void ? FormulaType.Void : FormulaType.Blank);
}
else
{
output = returnType == FormulaType.Void ? FormulaValue.NewVoid() : FormulaValue.NewBlank();
}

return output;
}

private static async Task<RecordValue> BuildReplacementRecordAsync(TableValue tableValue, RecordValue newRecord, CancellationToken cancellationToken)
{
var fields = new List<NamedValue>();

foreach (var fieldName in tableValue.Type.FieldNames)
{
cancellationToken.ThrowIfCancellationRequested();
FormulaValue value;

if (newRecord.Type.FieldNames.Contains(fieldName, StringComparer.Ordinal))
{
value = await newRecord.GetFieldAsync(fieldName, cancellationToken).ConfigureAwait(false);
}
else
{
value = FormulaValue.NewBlank(tableValue.Type.GetFieldType(fieldName));
}

fields.Add(new NamedValue(fieldName, value));
}

return FormulaValue.NewRecordFromFields(tableValue.Type.ToRecord(), fields);
}
}
}
20 changes: 20 additions & 0 deletions src/strings/PowerFxResources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -3322,6 +3322,26 @@
<value>If provided, last argument must be 'RemoveFlags.All'. Is there a typo?</value>
<comment>{Locked=RemoveFlags.All} Error Message, RemoveFlags.All is an enum value that does not get localized.</comment>
</data>
<data name="AboutUpdate" xml:space="preserve">
<value>Replaces a record in a data source.</value>
<comment>Description of 'Update' function.</comment>
</data>
<data name="UpdateDataSourceArg" xml:space="preserve">
<value>data_source</value>
<comment>function_parameter - First parameter for the Update function. The data source that contains the record that you want to replace. Translate this string. When translating, maintain as a single word (i.e., do not add spaces).</comment>
</data>
<data name="UpdateOldRecordArg" xml:space="preserve">
<value>old_record</value>
<comment>function_parameter - Second parameter for the Update function. The record to replace. Translate this string. When translating, maintain as a single word (i.e., do not add spaces).</comment>
</data>
<data name="UpdateNewRecordArg" xml:space="preserve">
<value>new_record</value>
<comment>function_parameter - Third parameter for the Update function. The replacement record. Translate this string. When translating, maintain as a single word (i.e., do not add spaces).</comment>
</data>
<data name="UpdateAllArg" xml:space="preserve">
<value>all</value>
<comment>function_parameter - Optional fourth parameter for the Update function. Indicates all matching records should be updated. Translate this string. When translating, maintain as a single word (i.e., do not add spaces).</comment>
</data>
<data name="NotificationType_Error_DisplayName" xml:space="preserve">
<value>Error</value>
<comment>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.</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,35 @@ public void AppendErrorTests()
Assert.IsType<ErrorValue>(result);
}

[Fact]
public async Task UpdateAllUpdatesEachPrimaryKeyMatchOnce()
{
var rows = new List<TestDatabaseRecordValue>()
{
new TestDatabaseRecordValue(1, "first", "old"),
new TestDatabaseRecordValue(1, "second", "old"),
new TestDatabaseRecordValue(2, "third", "old")
};

var table = new KeyedCollectionTableValue(rows);
var oldRecord = new TestDatabaseRecordValue(1, "ignored", "ignored");
var replacementRecord = FormulaValue.NewRecordFromFields(
TestDatabaseRecordValue.CustomRecordType,
new NamedValue("Id", FormulaValue.New(1)),
new NamedValue("Name", FormulaValue.New("updated")),
new NamedValue("Val", FormulaValue.New("new")));

var result = await table.UpdateAsync(oldRecord, replacementRecord, all: true, CancellationToken.None);

Assert.True(result.IsValue);
Assert.Equal("updated", rows[0].Name);
Assert.Equal("new", rows[0].Val);
Assert.Equal("updated", rows[1].Name);
Assert.Equal("new", rows[1].Val);
Assert.Equal("third", rows[2].Name);
Assert.Equal("old", rows[2].Val);
}

/// <summary>
/// Meant to test PatchSingleRecordCoreAsync override. Only tables with primary key column are supported.
/// </summary>
Expand Down Expand Up @@ -622,6 +651,28 @@ public override Task<DValue<RecordValue>> AppendAsync(RecordValue record, Cancel
}
}

internal class KeyedCollectionTableValue : CollectionTableValue<TestDatabaseRecordValue>
{
public KeyedCollectionTableValue(IList<TestDatabaseRecordValue> records)
: base(TestDatabaseRecordValue.CustomRecordType, records)
{
}

protected override DValue<RecordValue> Marshal(TestDatabaseRecordValue item)
{
return DValue<RecordValue>.Of(item);
}

protected override TestDatabaseRecordValue MarshalInverse(RecordValue row)
{
var id = Convert.ToInt32(((DecimalValue)row.GetField("Id")).Value);
var name = ((StringValue)row.GetField("Name")).Value;
var val = ((StringValue)row.GetField("Val")).Value;

return new TestDatabaseRecordValue(id, name, val);
}
}

internal class FileObjectRecordValue : InMemoryRecordValue
{
public string SomeProperty { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#SETUP: PowerFxV1CompatibilityRules

>> Set(list, Table({ Name: "One", ID: 1, Stock: 10}, { Name: "Two", ID: 2, Stock: 20}))
Table({ID:1,Name:"One",Stock:10},{ID:2,Name:"Two",Stock:20})

>> Update(list, First(list), { Name: "Uno", ID: 1})
If(true, {test:1}, "Void value (result of the expression can't be used).")

>> list
Table({ID:1,Name:"Uno",Stock:Blank()},{ID:2,Name:"Two",Stock:20})

>> Set(dupes, Table({ Name: "Same", ID: 1}, { Name: "Same", ID: 1}, { Name: "Other", ID: 2}))
Table({ID:1,Name:"Same"},{ID:1,Name:"Same"},{ID:2,Name:"Other"})

>> Update(dupes, { Name: "Same", ID: 1}, { Name: "Changed", ID: 9}, "ALL")
If(true, {test:1}, "Void value (result of the expression can't be used).")

>> dupes
Table({ID:9,Name:"Changed"},{ID:9,Name:"Changed"},{ID:2,Name:"Other"})

>> Update(dupes, { Name: "Missing", ID: 3}, { Name: "Nope", ID: 0})
Error({Kind:ErrorKind.NotFound})
Loading