From d0e2f0d77de7724ef7fb75e65a5647ef8bc8d588 Mon Sep 17 00:00:00 2001 From: Anderson Silva Date: Fri, 7 Feb 2025 11:38:39 -0600 Subject: [PATCH] Moving dependency scan to CORE (#2827) Issue https://github.com/microsoft/Power-Fx/issues/2809. --- .../Functions/TexlFunction.cs | 22 ++ .../Microsoft.PowerFx.Core/Functions/Utils.cs | 17 + .../Microsoft.PowerFx.Core/IR/IRTranslator.cs | 9 +- .../IR/Visitors/DependencyVisitor.cs | 296 ++++++++++++++++++ .../Public/CheckResult.cs | 23 +- .../Microsoft.PowerFx.Core/Public/Engine.cs | 1 - .../Texl/Builtins/Collect.cs | 41 ++- .../Texl/Builtins/Join.cs | 32 ++ .../Texl/Builtins/Patch.cs | 92 +++++- .../Texl/Builtins/RenameColumns.cs | 13 + .../Texl/Builtins/ShowDropColumnsBase.cs | 10 + .../Texl/Builtins/SortByColumns.cs | 13 +- .../Texl/Builtins/Summarize.cs | 22 ++ .../Functions/Mutation/RemoveFunction.cs | 25 +- .../Functions/SetFunction.cs | 7 +- .../TestDVEntity.cs | 1 + .../PublicSurfaceTests.cs | 3 +- .../DependencyTests.cs | 284 +++++++++++++++++ 18 files changed, 894 insertions(+), 17 deletions(-) create mode 100644 src/libraries/Microsoft.PowerFx.Core/IR/Visitors/DependencyVisitor.cs create mode 100644 src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/DependencyTests.cs diff --git a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs index 5778346a41..e739d8dabf 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs @@ -21,6 +21,7 @@ using Microsoft.PowerFx.Core.Functions.FunctionArgValidators; using Microsoft.PowerFx.Core.Functions.Publish; using Microsoft.PowerFx.Core.Functions.TransportSchemas; +using Microsoft.PowerFx.Core.IR; using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.IR.Symbols; using Microsoft.PowerFx.Core.Localization; @@ -31,6 +32,7 @@ using Microsoft.PowerFx.Intellisense; using Microsoft.PowerFx.Syntax; using Microsoft.PowerFx.Types; +using static Microsoft.PowerFx.Core.IR.DependencyVisitor; using static Microsoft.PowerFx.Core.IR.IRTranslator; using CallNode = Microsoft.PowerFx.Syntax.CallNode; using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; @@ -1738,5 +1740,25 @@ internal ArgPreprocessor GetGenericArgPreprocessor(int index) return ArgPreprocessor.None; } + + /// + /// Visit all function nodes to compose dependency info. + /// + /// IR CallNode. + /// Dependency visitor. + /// Dependency context. + /// + public virtual bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyContext context) + { + foreach (var arg in node.Args) + { + arg.Accept(visitor, context); + } + + // The return value is used by DepedencyScanFunctionTests test case. + // Returning false to indicate that the function runs a basic dependency scan. + // Other functions can override this method to return true if they have a custom dependency scan. + return false; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Functions/Utils.cs b/src/libraries/Microsoft.PowerFx.Core/Functions/Utils.cs index 0e3189f57b..f4d8ae1273 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Functions/Utils.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Functions/Utils.cs @@ -2,10 +2,14 @@ // Licensed under the MIT license. using System.Globalization; +using System.Linq; using System.Numerics; +using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; +using Microsoft.PowerFx.Types; namespace Microsoft.PowerFx.Core.Functions { @@ -23,5 +27,18 @@ public static string GetLocalizedName(this FunctionCategories category, CultureI { return StringResources.Get(category.ToString(), culture.Name); } + + public static void FunctionSupportColumnNamesAsIdentifiersDependencyUtil(this CallNode node, DependencyVisitor visitor) + { + var aggregateType0 = node.Args[0].IRContext.ResultType as AggregateType; + + foreach (TextLiteralNode arg in node.Args.Skip(1).Where(a => a is TextLiteralNode)) + { + if (aggregateType0.TryGetFieldType(arg.LiteralValue, out _)) + { + visitor.AddDependency(aggregateType0.TableSymbolName, arg.LiteralValue); + } + } + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs b/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs index f6619a7639..13d7117790 100644 --- a/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs +++ b/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs @@ -29,8 +29,13 @@ namespace Microsoft.PowerFx.Core.IR { internal class IRResult - { - public IntermediateNode TopNode; + { + // IR top node after transformations. + public IntermediateNode TopNode; + + // Original IR node, without transformations. + public IntermediateNode TopOriginalNode; + public ScopeSymbol RuleScopeSymbol; } diff --git a/src/libraries/Microsoft.PowerFx.Core/IR/Visitors/DependencyVisitor.cs b/src/libraries/Microsoft.PowerFx.Core/IR/Visitors/DependencyVisitor.cs new file mode 100644 index 0000000000..9a754f7bc6 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Core/IR/Visitors/DependencyVisitor.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.PowerFx.Core.IR.Nodes; +using Microsoft.PowerFx.Core.IR.Symbols; +using Microsoft.PowerFx.Core.Types; +using Microsoft.PowerFx.Core.Utils; +using Microsoft.PowerFx.Types; +using static Microsoft.PowerFx.Syntax.PrettyPrintVisitor; + +namespace Microsoft.PowerFx.Core.IR +{ + // IR has already: + // - resolved everything to logical names. + // - resolved implicit ThisRecord + internal sealed class DependencyVisitor : IRNodeVisitor + { + // Track reults. + public DependencyInfo Info { get; private set; } = new DependencyInfo(); + + public DependencyVisitor() + { + } + + public override RetVal Visit(TextLiteralNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(NumberLiteralNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(BooleanLiteralNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(DecimalLiteralNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(ColorLiteralNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(RecordNode node, DependencyContext context) + { + // Visit all field values in case there are CallNodes. Field keys should be handled by the function caller. + foreach (var kv in node.Fields) + { + kv.Value.Accept(this, context); + } + + return null; + } + + public override RetVal Visit(ErrorNode node, DependencyContext context) + { + return null; + } + + public override RetVal Visit(LazyEvalNode node, DependencyContext context) + { + return node.Child.Accept(this, context); + } + + private readonly Dictionary _scopeTypes = new Dictionary(); + + public override RetVal Visit(CallNode node, DependencyContext context) + { + if (node.Scope != null) + { + // Functions with more complex scoping will be handled by the function itself. + var arg0 = node.Args[0]; + _scopeTypes[node.Scope.Id] = arg0.IRContext.ResultType; + } + + node.Function.ComposeDependencyInfo(node, this, context); + + return null; + } + + public override RetVal Visit(BinaryOpNode node, DependencyContext context) + { + node.Left.Accept(this, context); + node.Right.Accept(this, context); + return null; + } + + public override RetVal Visit(UnaryOpNode node, DependencyContext context) + { + return node.Child.Accept(this, context); + } + + public override RetVal Visit(ScopeAccessNode node, DependencyContext context) + { + // Could be a symbol from RowScope. + // Price in "LookUp(t1,Price=255)" + if (node.Value is ScopeAccessSymbol sym) + { + if (_scopeTypes.TryGetValue(sym.Parent.Id, out var type)) + { + // Ignore ThisRecord scopeaccess node. e.g. Summarize(table, f1, Sum(ThisGroup, f2)) where ThisGroup should be ignored. + if (type is TableType tableType && tableType.TryGetFieldType(sym.Name.Value, out _)) + { + AddDependency(tableType.TableSymbolName, sym.Name.Value); + + return null; + } + } + } + + return null; + } + + // field // IR will implicity recognize as ThisRecod.field + // ThisRecord.field // IR will get type of ThisRecord + // First(Remote).Data // IR will get type on left of dot. + public override RetVal Visit(RecordFieldAccessNode node, DependencyContext context) + { + node.From.Accept(this, context); + + var ltype = node.From.IRContext.ResultType; + if (ltype is RecordType ltypeRecord) + { + // Logical name of the table on left side. + // This will be null for non-dataverse records + var tableLogicalName = ltypeRecord.TableSymbolName; + if (tableLogicalName != null) + { + var fieldLogicalName = node.Field.Value; + AddDependency(tableLogicalName, fieldLogicalName); + } + } + + return null; + } + + public override RetVal Visit(ResolvedObjectNode node, DependencyContext context) + { + if (node.IRContext.ResultType is AggregateType aggregateType) + { + AddDependency(aggregateType.TableSymbolName, null); + } + + CheckResolvedObjectNodeValue(node, context); + + return null; + } + + public void CheckResolvedObjectNodeValue(ResolvedObjectNode node, DependencyContext context) + { + if (node.Value is NameSymbol sym) + { + if (sym.Owner is SymbolTableOverRecordType symTable) + { + RecordType type = symTable.Type; + var tableLogicalName = type.TableSymbolName; + + if (symTable.IsThisRecord(sym)) + { + // "ThisRecord". Whole entity + AddDependency(type.TableSymbolName, null); + return; + } + + // on current table + var fieldLogicalName = sym.Name; + + AddDependency(type.TableSymbolName, fieldLogicalName); + } + } + } + + public override RetVal Visit(SingleColumnTableAccessNode node, DependencyContext context) + { + throw new NotImplementedException(); + } + + public override RetVal Visit(ChainingNode node, DependencyContext context) + { + foreach (var child in node.Nodes) + { + child.Accept(this, context); + } + + return null; + } + + public override RetVal Visit(AggregateCoercionNode node, DependencyContext context) + { + foreach (var kv in node.FieldCoercions) + { + kv.Value.Accept(this, context); + } + + return null; + } + + public class RetVal + { + } + + public class DependencyContext + { + public DependencyContext() + { + } + } + + // if fieldLogicalName, then we're taking a dependency on entire record. + public void AddDependency(string tableLogicalName, string fieldLogicalName) + { + if (tableLogicalName == null) + { + return; + } + + if (!Info.Dependencies.ContainsKey(tableLogicalName)) + { + Info.Dependencies[tableLogicalName] = new HashSet(); + } + + if (fieldLogicalName != null) + { + Info.Dependencies[tableLogicalName].Add(fieldLogicalName); + } + } + } + + /// + /// Capture Dataverse field-level reads and writes within a formula. + /// + public class DependencyInfo + { +#pragma warning disable CS1570 // XML comment has badly formed XML + /// + /// A dictionary of field logical names on related records, indexed by the related entity logical name. + /// + /// + /// On account, the formula "Name & 'Primary Contact'.'Full Name'" would return + /// "contact" => { "fullname" } + /// The formula "Name & 'Primary Contact'.'Full Name' & Sum(Contacts, 'Number Of Childeren')" would return + /// "contact" => { "fullname", "numberofchildren" }. + /// + public Dictionary> Dependencies { get; set; } + + public DependencyInfo() + { + Dependencies = new Dictionary>(); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + DumpHelper(sb, Dependencies); + + return sb.ToString(); + } + + private static void DumpHelper(StringBuilder sb, Dictionary> dict) + { + if (dict != null) + { + foreach (var kv in dict) + { + sb.Append("Entity"); + sb.Append(" "); + sb.Append(kv.Key); + sb.Append(": "); + + bool first = true; + foreach (var x in kv.Value) + { + if (!first) + { + sb.Append(", "); + } + + first = false; + sb.Append(x); + } + + sb.AppendLine("; "); + } + } + } + } +} diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/CheckResult.cs b/src/libraries/Microsoft.PowerFx.Core/Public/CheckResult.cs index fe999fd86b..862778772f 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/CheckResult.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/CheckResult.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Logging; using Microsoft.PowerFx.Core.Public; @@ -480,6 +481,23 @@ private void VerifyReturnTypeMatch() _errors.AddRange(ftErrors); } + /// + /// Compute the dependencies. Called after binding. + /// + [Obsolete("Preview")] + public DependencyInfo ApplyDependencyInfoScan() + { + var ir = ApplyIR(); //throws on errors + + var ctx = new DependencyVisitor.DependencyContext(); + var visitor = new DependencyVisitor(); + + // Using the original node without transformations. This simplifies the dependency analysis for PFx.DV side. + ir.TopOriginalNode.Accept(visitor, ctx); + + return visitor.Info; + } + /// /// Compute the dependencies. Called after binding. /// @@ -539,6 +557,8 @@ internal IRResult ApplyIR() this.ThrowOnErrors(); (var irnode, var ruleScopeSymbol) = IRTranslator.Translate(binding); + var originalIRNode = irnode; + var list = _engine.IRTransformList; if (list != null) { @@ -555,6 +575,7 @@ internal IRResult ApplyIR() _irresult = new IRResult { TopNode = irnode, + TopOriginalNode = originalIRNode, RuleScopeSymbol = ruleScopeSymbol }; } @@ -591,7 +612,7 @@ public FormulaType GetNodeType(TexlNode node) /// /// /// Null if the node is not bound. - public FunctionInfo GetFunctionInfo(CallNode node) + public FunctionInfo GetFunctionInfo(Syntax.CallNode node) { if (node == null) { diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs index 575e98dca4..23a1261ad2 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Reflection; using Microsoft.PowerFx.Core; -using Microsoft.PowerFx.Core.Annotations; using Microsoft.PowerFx.Core.App.Controls; using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.Entities.QueryOptions; diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Collect.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Collect.cs index 297315def5..05fe602e7d 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Collect.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Collect.cs @@ -19,7 +19,8 @@ using Microsoft.PowerFx.Syntax; using Microsoft.PowerFx.Types; using CallNode = Microsoft.PowerFx.Syntax.CallNode; -using RecordNode = Microsoft.PowerFx.Core.IR.Nodes.RecordNode; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; +using IRRecordNode = Microsoft.PowerFx.Core.IR.Nodes.RecordNode; namespace Microsoft.PowerFx.Core.Texl.Builtins { @@ -450,7 +451,7 @@ protected static List CreateIRCallNodeCollect(CallNode node, I if (arg.IRContext.ResultType._type.IsPrimitive) { newArgs.Add( - new RecordNode( + new IRRecordNode( new IRContext(arg.IRContext.SourceContext, RecordType.Empty().Add(TableValue.ValueName, arg.IRContext.ResultType)), new Dictionary { @@ -464,6 +465,34 @@ protected static List CreateIRCallNodeCollect(CallNode node, I } return newArgs; + } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType = (TableType)node.Args[0].IRContext.ResultType; + + foreach (var arg in node.Args.Skip(1)) + { + var argType = arg.IRContext.ResultType; + string argTableSymbolName = null; + + if (argType is AggregateType aggregateType) + { + // If argN is a record/table and has a table symbol name, it means we are referencing another entity. + // We then need to add a dependency to the table symbol name as well. + argTableSymbolName = aggregateType.TableSymbolName; + } + + foreach (var name in argType._type.GetAllNames(DPath.Root)) + { + visitor.AddDependency(tableType.TableSymbolName, name.Name.Value); + visitor.AddDependency(argTableSymbolName, name.Name.Value); + } + + arg.Accept(visitor, context); + } + + return true; } } @@ -489,6 +518,14 @@ public override DType GetCollectedType(Features features, DType argType) internal override IntermediateNode CreateIRCallNode(PowerFx.Syntax.CallNode node, IRTranslator.IRTranslatorContext context, List args, ScopeSymbol scope) { return base.CreateIRCallNode(node, context, CreateIRCallNodeCollect(node, context, args, scope), scope); + } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType = (TableType)node.Args[0].IRContext.ResultType; + visitor.AddDependency(tableType.TableSymbolName, "Value"); + + return true; } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Join.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Join.cs index 4ed7a644d3..892cde5428 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Join.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Join.cs @@ -238,5 +238,37 @@ internal override IntermediateNode CreateIRCallNode(PowerFx.Syntax.CallNode node return new IRCallNode(context.GetIRContext(node), this, scope, newArgs); } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + // Skipping args 0 and 1. + for (int i = 2; i < node.Args.Count; i++) + { + var arg = node.Args[i]; + RecordNode recordNode; + + switch (i) + { + case 2: // Predicate arg. + arg.Accept(visitor, context); + break; + case 5: + case 6: // Left and right record args. + var sourceArg = node.Args[i - 5]; + recordNode = arg as RecordNode; + foreach (var field in recordNode.Fields) + { + var tableType = (TableType)sourceArg.IRContext.ResultType; + visitor.AddDependency(tableType.TableSymbolName, field.Key.Value); + } + + break; + default: + break; + } + } + + return true; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Patch.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Patch.cs index 810f781300..738f310bd1 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Patch.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Patch.cs @@ -10,14 +10,15 @@ using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; using Microsoft.PowerFx.Core.Functions.DLP; -using Microsoft.PowerFx.Core.IR.Nodes; -using Microsoft.PowerFx.Core.IR.Symbols; +using Microsoft.PowerFx.Core.IR; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Syntax; -using static Microsoft.PowerFx.Core.IR.IRTranslator; +using Microsoft.PowerFx.Types; using CallNode = Microsoft.PowerFx.Syntax.CallNode; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; +using IRRecordNode = Microsoft.PowerFx.Core.IR.Nodes.RecordNode; namespace Microsoft.PowerFx.Core.Texl.Builtins { @@ -447,6 +448,35 @@ public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[ MutationUtils.CheckForReadOnlyFields(argTypes[0], args.Skip(2).ToArray(), argTypes.Skip(2).ToArray(), errors); } } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType = (TableType)node.Args[0].IRContext.ResultType; + var recordType = (RecordType)node.Args[1].IRContext.ResultType; + + // arg1 might refere both to arg0 and arg1. + // Examples: + // Patch(t1, {...}, {...}) => arg1 is inmemory record and the fields refers to t1. + // Patch(t1, First(t2), {...}) => arg1 fields refers to t2 and t1. + foreach (var fieldName in recordType.FieldNames) + { + visitor.AddDependency(recordType.TableSymbolName, fieldName); + } + + foreach (var arg in node.Args.Skip(1)) + { + arg.Accept(visitor, context); + if (arg.IRContext.ResultType is AggregateType aggregateType) + { + foreach (var fieldName in aggregateType.FieldNames) + { + visitor.AddDependency(tableType.TableSymbolName, fieldName); + } + } + } + + return true; + } } // Patch(DS, record_with_keys_and_updates) @@ -474,6 +504,21 @@ public PatchSingleRecordFunction() { yield return new[] { TexlStrings.PatchArg_Source, TexlStrings.PatchArg_Record }; } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType = (TableType)node.Args[0].IRContext.ResultType; + var recordType = (RecordType)node.Args[1].IRContext.ResultType; + + var datasource = tableType._type.AssociatedDataSources.First(); + + foreach (var fieldName in recordType.FieldNames) + { + visitor.AddDependency(tableType.TableSymbolName, fieldName); + } + + return true; + } } // Patch(DS, table_of_rows, table_of_updates) @@ -500,6 +545,25 @@ public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[ MutationUtils.CheckForReadOnlyFields(argTypes[0], args.Skip(2).ToArray(), argTypes.Skip(2).ToArray(), errors); } } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType0 = (TableType)node.Args[0].IRContext.ResultType; + var tableType1 = (TableType)node.Args[1].IRContext.ResultType; + var tableType2 = (TableType)node.Args[2].IRContext.ResultType; + + foreach (var fieldName in tableType1.FieldNames) + { + visitor.AddDependency(tableType0.TableSymbolName, fieldName); + } + + foreach (var fieldName in tableType2.FieldNames) + { + visitor.AddDependency(tableType0.TableSymbolName, fieldName); + } + + return true; + } } // Patch(DS, table_of_rows_with_updates) @@ -516,6 +580,28 @@ public PatchAggregateSingleTableFunction() { yield return new[] { TexlStrings.PatchArg_Source, TexlStrings.PatchArg_Rows }; } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType0 = (TableType)node.Args[0].IRContext.ResultType; + var tableType1 = (TableType)node.Args[1].IRContext.ResultType; + + var datasource = tableType0._type.AssociatedDataSources.First(); + + foreach (var fieldName in tableType1.FieldNames) + { + if (datasource != null && datasource.GetKeyColumns().Contains(fieldName)) + { + visitor.AddDependency(tableType0.TableSymbolName, fieldName); + } + else + { + visitor.AddDependency(tableType0.TableSymbolName, fieldName); + } + } + + return true; + } } // Patch(Record, Updates1, Updates2,…) diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/RenameColumns.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/RenameColumns.cs index 7044d0d9a5..3ce283dc20 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/RenameColumns.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/RenameColumns.cs @@ -3,15 +3,21 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Microsoft.PowerFx.Core.App.ErrorContainers; using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.Entities.QueryOptions; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; +using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Syntax; +using Microsoft.PowerFx.Types; +using CallNode = Microsoft.PowerFx.Syntax.CallNode; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; namespace Microsoft.PowerFx.Core.Texl.Builtins { @@ -265,5 +271,12 @@ public static ColumnReplacement Create(DName oldName, DName newName, DType type) }; } } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + node.FunctionSupportColumnNamesAsIdentifiersDependencyUtil(visitor); + + return true; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/ShowDropColumnsBase.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/ShowDropColumnsBase.cs index c9b315149a..b080db4f2c 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/ShowDropColumnsBase.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/ShowDropColumnsBase.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Linq; using Microsoft.PowerFx.Core.App.ErrorContainers; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; +using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; @@ -193,5 +196,12 @@ public override ParamIdentifierStatus GetIdentifierParamStatus(TexlNode node, Fe return index > 0 ? ParamIdentifierStatus.AlwaysIdentifier : ParamIdentifierStatus.NeverIdentifier; } + + public override bool ComposeDependencyInfo(IR.Nodes.CallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + node.FunctionSupportColumnNamesAsIdentifiersDependencyUtil(visitor); + + return true; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/SortByColumns.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/SortByColumns.cs index d2b9bff807..ec4463cd84 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/SortByColumns.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/SortByColumns.cs @@ -7,18 +7,22 @@ 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.Delegation; using Microsoft.PowerFx.Core.Functions.Delegation.DelegationMetadata; using Microsoft.PowerFx.Core.Functions.FunctionArgValidators; +using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Types.Enums; using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Syntax; +using Microsoft.PowerFx.Types; +using CallNode = Microsoft.PowerFx.Syntax.CallNode; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; namespace Microsoft.PowerFx.Core.Texl.Builtins { @@ -543,5 +547,12 @@ public override ParamIdentifierStatus GetIdentifierParamStatus(TexlNode node, Fe return (index % 2) == 1 ? ParamIdentifierStatus.PossiblyIdentifier : ParamIdentifierStatus.NeverIdentifier; } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + node.FunctionSupportColumnNamesAsIdentifiersDependencyUtil(visitor); + + return true; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Summarize.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Summarize.cs index f285151580..4cb08064dc 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Summarize.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Summarize.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.PowerFx.Core.App.ErrorContainers; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; @@ -259,6 +260,27 @@ internal override IntermediateNode CreateIRCallNode(CallNode node, IRTranslatorC } return new IRCallNode(context.GetIRContext(node), this, scope, newArgs); + } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableTypeName = ((AggregateType)node.Args[0].IRContext.ResultType).TableSymbolName; + + foreach (var arg in node.Args.Skip(1)) + { + if (arg is TextLiteralNode textLiteralNode) + { + visitor.AddDependency(tableTypeName, textLiteralNode.LiteralValue); + } + else if (arg is LazyEvalNode lazyEvalNode) + { + var recordNode = (RecordNode)lazyEvalNode.Child; + + recordNode.Fields.Values.First().Accept(visitor, context); + } + } + + return true; } } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs index 7ce73eeba3..cda01cf15f 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/Mutation/RemoveFunction.cs @@ -10,13 +10,13 @@ using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; -using Microsoft.PowerFx.Core.Localization; +using Microsoft.PowerFx.Core.IR; 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; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; namespace Microsoft.PowerFx.Functions { @@ -235,8 +235,27 @@ public async Task InvokeAsync(FunctionInvokeInfo invokeInfo, Cance { result = returnType == FormulaType.Void ? FormulaValue.NewVoid() : FormulaValue.NewBlank(); } - + return result; + } + + public override bool ComposeDependencyInfo(IRCallNode node, DependencyVisitor visitor, DependencyVisitor.DependencyContext context) + { + var tableType = (TableType)node.Args[0].IRContext.ResultType; + + foreach (var arg in node.Args.Skip(1).Where(a => a.IRContext.ResultType is AggregateType)) + { + var argType = arg.IRContext.ResultType; + + foreach (var name in argType._type.GetAllNames(DPath.Root)) + { + visitor.AddDependency(tableType.TableSymbolName, name.Name.Value); + } + + arg.Accept(visitor, context); + } + + return true; } } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/SetFunction.cs b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/SetFunction.cs index 1dfa1f1cc1..18323d4797 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/Functions/SetFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/Functions/SetFunction.cs @@ -2,16 +2,17 @@ // Licensed under the MIT license. using System.Collections.Generic; -using System.Linq; 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.IR; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Syntax; -using static Microsoft.PowerFx.Core.Localization.TexlStrings; +using static Microsoft.PowerFx.Core.Localization.TexlStrings; +using IRCallNode = Microsoft.PowerFx.Core.IR.Nodes.CallNode; namespace Microsoft.PowerFx.Interpreter { @@ -147,4 +148,4 @@ private bool CheckMutability(TexlBinding binding, TexlNode[] args, DType[] argTy return false; } } -} +} diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDVEntity.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDVEntity.cs index 177c71f5a3..8a6fc34153 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDVEntity.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDVEntity.cs @@ -76,6 +76,7 @@ public static DType GetDType(bool hasCachedCountRows = false) displayNameMapping.Add("address1_line1", "Address 1: Street 1"); displayNameMapping.Add("nonsearchablestringcol", "Non-searchable string column"); displayNameMapping.Add("nonsortablestringcolumn", "Non-sortable string column"); + displayNameMapping.Add("numberofemployees", "Number of employees"); return DType.AttachDataSourceInfo(accountsType, dataSource); } diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/PublicSurfaceTests.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/PublicSurfaceTests.cs index 484d1bd13c..ee70f4b2db 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/PublicSurfaceTests.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/PublicSurfaceTests.cs @@ -215,7 +215,8 @@ public void PublicSurface_Tests() "Microsoft.PowerFx.Logging.ITracer", "Microsoft.PowerFx.Logging.TraceSeverity", "Microsoft.PowerFx.PowerFxFileInfo", - "Microsoft.PowerFx.UserInfo" + "Microsoft.PowerFx.UserInfo", + "Microsoft.PowerFx.Core.IR.DependencyInfo", }; var sb = new StringBuilder(); diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/DependencyTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/DependencyTests.cs new file mode 100644 index 0000000000..06d39006e5 --- /dev/null +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/DependencyTests.cs @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Text; +using Microsoft.PowerFx.Core.Functions; +using Microsoft.PowerFx.Core.IR; +using Microsoft.PowerFx.Core.IR.Nodes; +using Microsoft.PowerFx.Core.Tests.AssociatedDataSourcesTests; +using Microsoft.PowerFx.Core.Tests.Helpers; +using Microsoft.PowerFx.Core.Texl; +using Microsoft.PowerFx.Core.Texl.Builtins; +using Microsoft.PowerFx.Core.Types; +using Microsoft.PowerFx.Core.Types.Enums; +using Microsoft.PowerFx.Functions; +using Microsoft.PowerFx.Interpreter; +using Microsoft.PowerFx.Types; +using Xunit; + +namespace Microsoft.PowerFx.Core.Tests +{ + public class DependencyTests : PowerFxTest + { + [Theory] + [InlineData("1+2", "")] // none + [InlineData("ThisRecord.'Address 1: City' & 'Account Name'", "Entity Accounts: address1_city, name;")] // basic read + + [InlineData("numberofemployees%", "Entity Accounts: numberofemployees;")] // unary op + [InlineData("ThisRecord", "Entity Accounts: ;")] // whole scope + [InlineData("{x:5}.x", "")] + [InlineData("With({x : ThisRecord}, x.'Address 1: City')", "Entity Accounts: address1_city;")] // alias + [InlineData("With({'Address 1: City' : \"Seattle\"}, 'Address 1: City' & 'Account Name')", "Entity Accounts: name;")] // 'Address 1: City' is shadowed + [InlineData("With({'Address 1: City' : 5}, ThisRecord.'Address 1: City')", "")] // shadowed + [InlineData("LookUp(local,'Address 1: City'=\"something\")", "Entity Accounts: address1_city;")] // Lookup and RowScope + [InlineData("Filter(local,numberofemployees > 200)", "Entity Accounts: numberofemployees;")] + [InlineData("First(local)", "Entity Accounts: ;")] + [InlineData("First(local).'Address 1: City'", "Entity Accounts: address1_city;")] + [InlineData("Last(local)", "Entity Accounts: ;")] + [InlineData("local", "Entity Accounts: ;")] // whole table + [InlineData("12 & true & \"abc\" ", "")] // walker ignores literals + [InlineData("12;'Address 1: City';12", "Entity Accounts: address1_city;")] // chaining + [InlineData("ParamLocal1.'Address 1: City'", "Entity Accounts: address1_city;")] // basic read + [InlineData("{test:First(local).name}", "Entity Accounts: name;")] + [InlineData("AddColumns(simple1, z, 1)", "Entity Simple1: ;")] + [InlineData("RenameColumns(simple1, b, c)", "Entity Simple1: b;")] + [InlineData("SortByColumns(simple1, a, SortOrder.Descending)", "Entity Simple1: a;")] + [InlineData("RenameColumns(If(false, simple1, Error({Kind:ErrorKind.Custom})), b, c)", "Entity Simple1: b;")] + [InlineData("SortByColumns(If(false, simple1, Error({Kind:ErrorKind.Custom})), a, SortOrder.Descending)", "Entity Simple1: a;")] + + // Basic scoping + [InlineData("Min(local,numberofemployees)", "Entity Accounts: numberofemployees;")] + [InlineData("Average(local,numberofemployees)", "Entity Accounts: numberofemployees;")] + + // Patch + [InlineData("Patch(simple2, First(simple1), { a : 1 })", "Entity Simple1: a, b; Entity Simple2: a, b;")] + [InlineData("Patch(local, {'Address 1: City':\"test\"}, { 'Account Name' : \"some name\"})", "Entity Accounts: address1_city, name;")] + [InlineData("Patch(local, {accountid:GUID(), 'Address 1: City':\"test\"})", "Entity Accounts: accountid, address1_city;")] + [InlineData("Patch(local, Table({accountid:GUID(), 'Address 1: City':\"test\"},{accountid:GUID(), 'Account Name':\"test\"}))", "Entity Accounts: accountid, address1_city, name;")] + [InlineData("Patch(local, Table({accountid:GUID(), 'Address 1: City':\"test\"},{accountid:GUID(), 'Account Name':\"test\"}),Table({'Address 1: City':\"test\"},{'Address 1: City':\"test\",'Account Name':\"test\"}))", "Entity Accounts: accountid, address1_city, name;")] + [InlineData("Patch(simple2, First(simple1), { a : First(simple1).b } )", "Entity Simple1: a, b; Entity Simple2: a, b;")] + [InlineData("Patch(simple1, First(simple1), { a : First(simple1).b } )", "Entity Simple1: a, b;")] + + // Remove + [InlineData("Remove(local, {name: First(remote).name})", "Entity Accounts: name; Entity Contacts: name;")] + + // Collect and ClearCollect. + [InlineData("Collect(local, Table({ 'Account Name' : \"some name\"}))", "Entity Accounts: name;")] + [InlineData("Collect(simple2, simple1)", "Entity Simple2: a, b; Entity Simple1: a, b;")] + [InlineData("Collect(simple2, { a : First(simple1).b })", "Entity Simple2: a; Entity Simple1: b;")] + [InlineData("Collect(local, { 'Address 1: City' : First(remote).'Contact Name' })", "Entity Accounts: address1_city; Entity Contacts: name;")] + [InlineData("ClearCollect(simple2, simple1)", "Entity Simple2: a, b; Entity Simple1: a, b;")] + [InlineData("ClearCollect(local, Table({ 'Account Name' : \"some name\"}))", "Entity Accounts: name;")] + + // Inside with. + [InlineData("With({r: local}, Filter(r, 'Number of employees' > 0))", "Entity Accounts: numberofemployees;")] + [InlineData("With({r: local}, LookUp(r, 'Number of employees' > 0))", "Entity Accounts: numberofemployees;")] + + // Option set. + [InlineData("Filter(local, dayofweek = StartOfWeek.Monday)", "Entity Accounts: dayofweek;")] + + [InlineData("Filter(ForAll(local, ThisRecord.numberofemployees), Value < 20)", "Entity Accounts: numberofemployees;")] + + // Summarize is special, becuase of ThisGroup. + [InlineData("Summarize(local, 'Account Name', Sum(ThisGroup, numberofemployees) As Employees)", "Entity Accounts: name, numberofemployees;")] + [InlineData("Summarize(local, 'Account Name', Sum(ThisGroup, numberofemployees * 2) As TPrice)", "Entity Accounts: name, numberofemployees;")] + + // Join + [InlineData("Join(remote As l, local As r, l.contactid = r.contactid, JoinType.Inner, r.name As AccountName)", "Entity Contacts: contactid; Entity Accounts: contactid, name;")] + [InlineData("Join(remote As l, local As r, l.contactid = r.contactid, JoinType.Inner, r.name As AccountName, l.contactnumber As NewContactNumber)", "Entity Contacts: contactid, contactnumber; Entity Accounts: contactid, name;")] + [InlineData("Join(remote, local, LeftRecord.contactid = RightRecord.contactid, JoinType.Inner, RightRecord.name As AccountName, LeftRecord.contactnumber As NewContactNumber)", "Entity Contacts: contactid, contactnumber; Entity Accounts: contactid, name;")] + + // Set + [InlineData("Set(numberofemployees, 200)", "Entity Accounts: numberofemployees;")] + [InlineData("Set('Address 1: City', 'Account Name')", "Entity Accounts: address1_city, name;")] + [InlineData("Set('Address 1: City', 'Address 1: City' & \"test\")", "Entity Accounts: address1_city;")] + [InlineData("Set(NewRecord.'Address 1: City', \"test\")", "Entity Accounts: address1_city;")] + + [InlineData("Filter(Distinct(ShowColumns(simple2, a, b), a), Value < 20)", "Entity Simple2: a, b;")] + [InlineData("Filter(Distinct(DropColumns(simple2, c), a), Value < 20)", "Entity Simple2: c, a;")] + + [InlineData("AddColumns(simple1, z, a+1)", "Entity Simple1: a;")] + public void GetDependencies(string expr, string expected) + { + var opt = new ParserOptions() { AllowsSideEffects = true }; + var engine = new Engine(); + + var check = new CheckResult(engine) + .SetText(expr, opt) + .SetBindingInfo(GetSymbols()); + + check.ApplyBinding(); + +#pragma warning disable CS0618 // Type or member is obsolete + var info = check.ApplyDependencyInfoScan(); +#pragma warning restore CS0618 // Type or member is obsolete + var actual = info.ToString().Replace("\r", string.Empty).Replace("\n", string.Empty).Trim(); + Assert.Equal(expected, actual); + } + + private ReadOnlySymbolTable GetSymbols() + { + var localType = Accounts(); + var remoteType = Contacts(); + var simple1Type = Simple1(); + var simple2Type = Simple2(); + var customSymbols = new SymbolTable { DebugName = "Custom symbols " }; + var opt = new ParserOptions() { AllowsSideEffects = true }; + + var thisRecordScope = ReadOnlySymbolTable.NewFromRecord(localType.ToRecord(), allowThisRecord: true, allowMutable: true); + + customSymbols.AddFunction(new JoinFunction()); + customSymbols.AddFunction(new CollectFunction()); + customSymbols.AddFunction(new CollectScalarFunction()); + customSymbols.AddFunction(new ClearCollectFunction()); + customSymbols.AddFunction(new ClearCollectScalarFunction()); + customSymbols.AddFunction(new PatchFunction()); + customSymbols.AddFunction(new PatchAggregateFunction()); + customSymbols.AddFunction(new PatchAggregateSingleTableFunction()); + customSymbols.AddFunction(new PatchSingleRecordFunction()); + customSymbols.AddFunction(new SummarizeFunction()); + customSymbols.AddFunction(new RecalcEngineSetFunction()); + customSymbols.AddFunction(new RemoveFunction()); + customSymbols.AddFunction(Library.DistinctInterpreterFunction); + customSymbols.AddVariable("local", localType, mutable: true); + customSymbols.AddVariable("remote", remoteType, mutable: true); + customSymbols.AddVariable("simple1", simple1Type, mutable: true); + customSymbols.AddVariable("simple2", simple2Type, mutable: true); + + // Simulate a parameter + var parameterSymbols = new SymbolTable { DebugName = "Parameters " }; + parameterSymbols.AddVariable("ParamLocal1", localType.ToRecord(), mutable: true); + parameterSymbols.AddVariable("NewRecord", localType.ToRecord(), new SymbolProperties() { CanMutate = false, CanSet = false, CanSetMutate = true }); + + return ReadOnlySymbolTable.Compose(customSymbols, thisRecordScope, parameterSymbols); + } + + private TableType Accounts() + { + var tableType = (TableType)FormulaType.Build(AccountsTypeHelper.GetDType()); + tableType = tableType.Add("dayofweek", BuiltInEnums.StartOfWeekEnum.FormulaType); + tableType = tableType.Add("contactid", FormulaType.Guid); + + return tableType; + } + + private TableType Contacts() + { + var simplifiedAccountsSchema = "*[contactid:g, contactnumber:s, name`Contact Name`:s, address1_addresstypecode:l, address1_city`Address 1: City`:s, address1_composite:s, address1_country:s, address1_county:s, address1_line1`Address 1: Street 1`:s, numberofemployees:n]"; + + DType contactType = TestUtils.DT2(simplifiedAccountsSchema); + var dataSource = new TestDataSource( + "Contacts", + contactType, + keyColumns: new[] { "contactid" }, + selectableColumns: new[] { "name", "address1_city", "contactid", "address1_country", "address1_line1" }, + hasCachedCountRows: false); + var displayNameMapping = dataSource.DisplayNameMapping; + displayNameMapping.Add("name", "Contact Name"); + displayNameMapping.Add("address1_city", "Address 1: City"); + displayNameMapping.Add("address1_line1", "Address 1: Street 1"); + displayNameMapping.Add("numberofemployees", "Number of employees"); + + contactType = DType.AttachDataSourceInfo(contactType, dataSource); + + return (TableType)FormulaType.Build(contactType); + } + + // Some test cases can produce a long list of dependencies. + // This method is used to simplify the schema for those cases. + private TableType Simple1() + { + var simplifiedchema = "*[a:w,b:w]"; + + DType type = TestUtils.DT2(simplifiedchema); + var dataSource = new TestDataSource( + "Simple1", + type, + keyColumns: new[] { "a" }); + + type = DType.AttachDataSourceInfo(type, dataSource); + + return (TableType)FormulaType.Build(type); + } + + private TableType Simple2() + { + var simplifiedchema = "*[a:w,b:w,c:s]"; + + DType type = TestUtils.DT2(simplifiedchema); + var dataSource = new TestDataSource( + "Simple2", + type, + keyColumns: new[] { "a" }); + + type = DType.AttachDataSourceInfo(type, dataSource); + + return (TableType)FormulaType.Build(type); + } + + /// + /// This test case is to ensure that all functions that are not self-contained or + /// have a scope info have been assessed and either added to the exception list or overrides . + /// + [Fact] + public void DepedencyScanFunctionTests() + { + var names = new List(); + var functions = new List(); + functions.AddRange(BuiltinFunctionsCore.BuiltinFunctionsLibrary); + + // These methods use default implementation of ComposeDependencyInfo and do not neeed to override it. + var exceptionList = new HashSet() + { + "AddColumns", + "Average", + "Concat", + "CountIf", + "Filter", + "ForAll", + "IfError", + "LookUp", + "Max", + "Min", + "Refresh", + "Search", + "Sort", + "StdevP", + "Set", + "Sum", + "Trace", + "VarP", + "With", + }; + + foreach (var func in functions) + { + if (!func.IsSelfContained || func.ScopeInfo != null) + { + var irContext = IRContext.NotInSource(FormulaType.String); + var node = new CallNode(irContext, func, new ErrorNode(irContext, "test")); + var visitor = new DependencyVisitor(); + var context = new DependencyVisitor.DependencyContext(); + var overwritten = func.ComposeDependencyInfo(node, visitor, context); + if (!overwritten && !exceptionList.Contains(func.Name)) + { + names.Add(func.Name); + } + } + } + + if (names.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine("The following functions do not have a dependency scan:"); + foreach (var name in names) + { + sb.AppendLine(name); + } + + Assert.Fail(sb.ToString()); + } + } + } +}