From eff6346d6efd4ed4069ccef4324b4e16aec1a8ed Mon Sep 17 00:00:00 2001 From: Anderson Ferreira da Silva Date: Thu, 30 Apr 2026 22:47:39 -0500 Subject: [PATCH 1/2] Fix #2973: SortByColumns runtime error when called through UDF with missing columns When SortByColumns is called through a UDF that accepts a typed table, the actual runtime table may omit columns that are optional in the UDF parameter type. The prior code validated sort column names against arg0.Type (the runtime table type), causing a false "column does not exist" error for any missing column. Fix: validate against irContext.ResultType (the compile-time return type), which equals the binder's argTypes[0] and includes all binder-validated columns. Missing fields are already returned as Blank by RecordValue.GetFieldAsync, so sort order is correct. Also bumps .Net7.0 test project TFMs to net8.0 (net7.0 EOL on this machine). Fixes https://github.com/microsoft/Power-Fx/issues/2973 --- .../Functions/LibraryTable.cs | 15 +++++++-- .../Microsoft.PowerFx.Connectors.Tests.csproj | 2 +- ...Microsoft.PowerFx.Interpreter.Tests.csproj | 2 +- .../RecalcEngineTests.cs | 33 +++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) 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/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj b/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj index 612da8973b..9fde9499db 100644 --- a/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj +++ b/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 false false Debug;Release;Debug462;Debug70;DebugAll;Release462;Release70;ReleaseAll diff --git a/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj b/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj index ddd3feeadf..a86b8502e1 100644 --- a/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj +++ b/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 false false Debug;Release;Debug462;Debug70;DebugAll;Release462;Release70;ReleaseAll 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(); From ee6b08dc240ce6be7b0b1fc34fc08f807d92b6a7 Mon Sep 17 00:00:00 2001 From: Anderson Ferreira da Silva Date: Thu, 30 Apr 2026 22:54:17 -0500 Subject: [PATCH 2/2] Revert net8.0 -> net7.0 in .Net7.0 test project TFMs --- .../Microsoft.PowerFx.Connectors.Tests.csproj | 2 +- .../Microsoft.PowerFx.Interpreter.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj b/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj index 9fde9499db..612da8973b 100644 --- a/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj +++ b/src/tests/.Net7.0/Microsoft.PowerFx.Connectors.Tests/Microsoft.PowerFx.Connectors.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + net7.0 false false Debug;Release;Debug462;Debug70;DebugAll;Release462;Release70;ReleaseAll diff --git a/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj b/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj index a86b8502e1..ddd3feeadf 100644 --- a/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj +++ b/src/tests/.Net7.0/Microsoft.PowerFx.Interpreter.Tests/Microsoft.PowerFx.Interpreter.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + net7.0 false false Debug;Release;Debug462;Debug70;DebugAll;Release462;Release70;ReleaseAll