diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryTable.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryTable.cs index d53d204078..31f8352fce 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryTable.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/LibraryTable.cs @@ -894,7 +894,12 @@ public static async ValueTask SortByColumns(EvalVisitor runner, Ev { var columnName = ((StringValue)args[i]).Value; - if (!arg0.Type.FieldNames.Contains(columnName)) + // Use the compile-time result type (irContext.ResultType) rather than the + // actual runtime arg0.Type, which may be narrower when this function is + // called through a UDF with a table that omits optional columns. + // Missing columns are returned as Blank by RecordValue.GetFieldAsync. + // See https://github.com/microsoft/Power-Fx/issues/2973 + if (!((TableType)irContext.ResultType).FieldNames.Contains(columnName)) { return CreateInvalidSortColumnError(irContext, runner.CultureInfo, columnName); } @@ -961,7 +966,13 @@ public static async ValueTask SortByColumnsOrderTable(EvalVisitor { var arg0 = (TableValue)args[0]; var columnName = ((StringValue)args[1]).Value; - if (!arg0.Type.FieldNames.Contains(columnName)) + + // Use the compile-time result type (irContext.ResultType) rather than the + // actual runtime arg0.Type, which may be narrower when this function is + // called through a UDF with a table that omits optional columns. + // Missing columns are returned as Blank by RecordValue.GetFieldAsync. + // See https://github.com/microsoft/Power-Fx/issues/2973 + if (!((TableType)irContext.ResultType).FieldNames.Contains(columnName)) { return CreateInvalidSortColumnError(irContext, runner.CultureInfo, columnName); } diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs index ebfe13c928..cbbcf5ab19 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs @@ -2379,6 +2379,39 @@ public void UDFAggregateInputErrorMessage(string userDefinitions, string evalExp Assert.Contains(expectedError, ex.InnerExceptions.First().Message); } + // Regression test for https://github.com/microsoft/Power-Fx/issues/2973 + // SortByColumns should not throw when called via a UDF and the actual argument table + // omits one of the sort columns. Missing columns should be treated as Blank. + [Fact] + public void SortByColumns_UDF_MissingField() + { + var engine = new RecalcEngine(); + + // Define types and UDF matching the issue repro exactly. + engine.AddUserDefinitions( + "Person := Type( { Name: Text, Age: Number } );" + + "People := Type( [ Person ] );" + + "SortedPeople(list:People):People = SortByColumns(list, \"Name\", SortOrder.Ascending, \"Age\", SortOrder.Ascending);"); + + // AC-1: Missing Age column - should succeed and return sorted table, not an error. + var result = engine.Eval("SortedPeople([{Name:\"John\"},{Name:\"Jane\"}])"); + Assert.IsNotType(result); + Assert.IsAssignableFrom(result); + var rows = ((TableValue)result).Rows.ToList(); + Assert.Equal(2, rows.Count); + Assert.Equal("Jane", ((StringValue)rows[0].Value.GetField("Name")).Value); + Assert.Equal("John", ((StringValue)rows[1].Value.GetField("Name")).Value); + + // AC-2: Full columns present - should continue to work. + result = engine.Eval("SortedPeople([{Name:\"John\", Age:30},{Name:\"Jane\", Age:40}])"); + Assert.IsNotType(result); + Assert.IsAssignableFrom(result); + rows = ((TableValue)result).Rows.ToList(); + Assert.Equal(2, rows.Count); + Assert.Equal("Jane", ((StringValue)rows[0].Value.GetField("Name")).Value); + Assert.Equal("John", ((StringValue)rows[1].Value.GetField("Name")).Value); + } + #region Test private readonly StringBuilder _updates = new StringBuilder();