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 @@ -48,26 +48,33 @@ private static TableValue ToTable(this PortableValue[] values)

if (formulaValues[0] is RecordValue recordValue)
{
return FormulaValue.NewTable(ParseRecordType(recordValue), formulaValues.OfType<RecordValue>());
return FormulaValue.NewTable(ParseRecordType(recordValue), formulaValues.OfType<RecordValue>().ToArray());
}

return
formulaValues[0] switch
{
PrimitiveValue<bool> => NewSingleColumnTable<bool>(),
PrimitiveValue<string> => NewSingleColumnTable<string>(),
PrimitiveValue<int> => NewSingleColumnTable<int>(),
PrimitiveValue<long> => NewSingleColumnTable<long>(),
PrimitiveValue<float> => NewSingleColumnTable<float>(),
PrimitiveValue<decimal> => NewSingleColumnTable<decimal>(),
PrimitiveValue<double> => NewSingleColumnTable<double>(),
PrimitiveValue<TimeSpan> => NewSingleColumnTable<TimeSpan>(),
PrimitiveValue<DateTime> => NewSingleColumnTable<DateTime>(),
_ => throw new DeclarativeModelException($"Unsupported table element type: {formulaValues[0].Type.GetType().Name}"),
};
FormulaType elementType = formulaValues[0] switch
{
PrimitiveValue<bool>
or PrimitiveValue<string>
or PrimitiveValue<int>
or PrimitiveValue<long>
or PrimitiveValue<decimal>
or PrimitiveValue<float>
or PrimitiveValue<double>
or PrimitiveValue<TimeSpan>
or PrimitiveValue<DateTime> => formulaValues[0].Type,
_ => throw new DeclarativeModelException($"Unsupported table element type: {formulaValues[0].Type.GetType().Name}"),
Comment thread
peibekwe marked this conversation as resolved.
};

TableValue NewSingleColumnTable<TValue>() =>
FormulaValue.NewSingleColumnTable(formulaValues.OfType<PrimitiveValue<TValue>>());
RecordType singleColumnType = RecordType.Empty().Add("Value", elementType);
RecordValue[] rows =
[
.. formulaValues.Select(
value =>
FormulaValue.NewRecordFromFields(
singleColumnType,
new NamedValue("Value", value))),
];
return FormulaValue.NewTable(singleColumnType, rows);
}

public static bool IsSystemType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue) where TValue : struct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,43 +32,74 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st
case TableChangeType.Add:
ValueExpression addItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
EvaluationResult<DataValue> addResult = this.Evaluator.GetValue(addItemValue);
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormula());
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
FormulaValue addValue = addResult.Value.ToFormula();
RecordType recordType = tableValue.Type.ToRecord();
RecordValue newRecord;
TableValue resultTable;
if (!recordType.FieldNames.Any() && !tableValue.Rows.Any())
{
newRecord = BuildRecordFromValue(addValue);
resultTable = FormulaValue.NewTable(newRecord.Type, newRecord);
}
else
{
newRecord = BuildRecord(recordType, addValue);
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
resultTable = tableValue;
}
await this.AssignAsync(variablePath, resultTable, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ResultVariable?.Path, newRecord, context).ConfigureAwait(false);
break;
case TableChangeType.Remove:
ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}");
EvaluationResult<DataValue> removeResult = this.Evaluator.GetValue(removeItemValue);
if (removeResult.Value is TableDataValue removeItemTable)
{
await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(variablePath, RecordValue.Empty(), context).ConfigureAwait(false);
await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ResultVariable?.Path, RecordValue.Empty(), context).ConfigureAwait(false);
}
break;
case TableChangeType.Clear:
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false);
await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
break;
case TableChangeType.TakeFirst:
RecordValue? firstRow = tableValue.Rows.FirstOrDefault()?.Value;
if (firstRow is not null)
{
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(variablePath, firstRow, context).ConfigureAwait(false);
await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ResultVariable?.Path, firstRow, context).ConfigureAwait(false);
}
else
{
await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
}
break;
case TableChangeType.TakeLast:
RecordValue? lastRow = tableValue.Rows.LastOrDefault()?.Value;
if (lastRow is not null)
{
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(variablePath, lastRow, context).ConfigureAwait(false);
await this.AssignAsync(variablePath, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ResultVariable?.Path, lastRow, context).ConfigureAwait(false);
}
else
{
await this.AssignAsync(this.Model.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
}
break;
}

return default;

static RecordValue BuildRecordFromValue(FormulaValue value) =>
value is RecordValue recordValue ?
recordValue :
FormulaValue.NewRecordFromFields(new NamedValue("Value", value));

static RecordValue BuildRecord(RecordType recordType, FormulaValue value)
{
return FormulaValue.NewRecordFromFields(recordType, GetValues());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,26 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
{
ValueExpression addItemValue = Throw.IfNull(addItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}");
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(addItemValue);
RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula());
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
FormulaValue addValue = expressionResult.Value.ToFormula();
RecordType recordType = tableValue.Type.ToRecord();
TableValue resultTable;
if (!recordType.FieldNames.Any() && !tableValue.Rows.Any())
{
RecordValue newRecord = BuildRecordFromValue(addValue);
resultTable = FormulaValue.NewTable(newRecord.Type, newRecord);
}
else
{
RecordValue newRecord = BuildRecord(recordType, addValue);
await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false);
resultTable = tableValue;
}
await this.AssignAsync(this.Model.ItemsVariable, resultTable, context).ConfigureAwait(false);
}
else if (changeType is ClearItemsOperation)
{
await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
}
else if (changeType is RemoveItemOperation removeItemOperation)
{
Expand All @@ -47,30 +59,45 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat
if (expressionResult.Value.ToFormula() is TableValue removeItemTable)
{
await tableValue.RemoveAsync(removeItemTable.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
}
}
else if (changeType is TakeLastItemOperation)
else if (changeType is TakeLastItemOperation takeLastOperation)
{
RecordValue? lastRow = tableValue.Rows.LastOrDefault()?.Value;
if (lastRow is not null)
{
await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, lastRow, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(takeLastOperation.ResultVariable?.Path, lastRow, context).ConfigureAwait(false);
}
else
{
await this.AssignAsync(takeLastOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
}
}
else if (changeType is TakeFirstItemOperation)
else if (changeType is TakeFirstItemOperation takeFirstOperation)
{
RecordValue? firstRow = tableValue.Rows.FirstOrDefault()?.Value;
if (firstRow is not null)
{
await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, firstRow, context).ConfigureAwait(false);
await this.AssignAsync(this.Model.ItemsVariable, tableValue, context).ConfigureAwait(false);
await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, firstRow, context).ConfigureAwait(false);
}
else
{
await this.AssignAsync(takeFirstOperation.ResultVariable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false);
}
}

return default;

static RecordValue BuildRecordFromValue(FormulaValue value) =>
value is RecordValue recordValue ?
recordValue :
FormulaValue.NewRecordFromFields(new NamedValue("Value", value));

static RecordValue BuildRecord(RecordType recordType, FormulaValue value)
{
return FormulaValue.NewRecordFromFields(recordType, GetValues());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
Expand Down Expand Up @@ -84,6 +86,97 @@ public void ListComplexType()
Assert.Equal("input", textValue.Value);
}

[Fact]
public void TableAsPortableUsesLegacyArrayShape()
{
// Arrange
RecordType recordType = RecordType.Empty().Add("Id", FormulaType.Decimal);
RecordValue record =
FormulaValue.NewRecordFromFields(
recordType,
new NamedValue("Id", FormulaValue.New(1)));
TableValue source = FormulaValue.NewTable(recordType, record);

// Act
object result = source.AsPortable();

// Assert
Assert.IsType<PortableValue[]>(result);
}

[Fact]
public void RecordAsPortableUsesLegacyDictionaryShape()
{
// Arrange
RecordValue source =
FormulaValue.NewRecordFromFields(
new NamedValue("Id", FormulaValue.New(1)));

// Act
object result = source.AsPortable();

// Assert
Assert.IsAssignableFrom<IDictionary<string, PortableValue>>(result);
}

[Fact]
public async Task LegacyRecordTableSupportsAppendAsync()
{
// Arrange
Dictionary<string, decimal>[] source = [new() { ["Id"] = 1 }];
TableValue restored = Assert.IsAssignableFrom<TableValue>(new PortableValue(source.AsPortable()).ToFormula());
RecordType recordType = restored.Type.ToRecord();
RecordValue newRecord =
FormulaValue.NewRecordFromFields(
recordType,
new NamedValue("Id", FormulaValue.New(2)));

// Act
await restored.AppendAsync(newRecord, CancellationToken.None);

// Assert
Assert.Equal(2, restored.Rows.Count());
}

[Fact]
public async Task LegacyPrimitiveTableSupportsAppendAsync()
{
// Arrange
string[] source = ["one"];
TableValue restored = Assert.IsAssignableFrom<TableValue>(new PortableValue(source.AsPortable()).ToFormula());
RecordType recordType = restored.Type.ToRecord();
RecordValue newRecord =
FormulaValue.NewRecordFromFields(
recordType,
new NamedValue("Value", FormulaValue.New("two")));

// Act
await restored.AppendAsync(newRecord, CancellationToken.None);

// Assert
string[] values = restored.Rows
.Select(row => Assert.IsType<StringValue>(row.Value.GetField("Value")).Value)
.ToArray();
Assert.Equal(["one", "two"], values);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void LegacyDateTablePreservesFormulaType(bool includeTime)
{
// Arrange
DateTime value = new(2026, 7, 27, includeTime ? 12 : 0, 0, 0, DateTimeKind.Utc);

// Act
TableValue restored = Assert.IsAssignableFrom<TableValue>(new PortableValue(new[] { value }.AsPortable()).ToFormula());

// Assert
FormulaType expectedType = includeTime ? FormulaType.DateTime : FormulaType.Date;
Assert.Equal(expectedType, restored.Type.GetFieldType("Value"));
Assert.Equal(expectedType, Assert.Single(restored.Rows).Value.GetField("Value").Type);
}

[Fact]
public void DictionaryType()
{
Expand Down
Loading
Loading