diff --git a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs index 0b240d1ebc..22a68519a0 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs @@ -17,6 +17,8 @@ using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Types; using static Microsoft.PowerFx.Connectors.Constants; + +#pragma warning disable SA1000 // The keyword 'new' should be followeed by a space namespace Microsoft.PowerFx.Connectors { @@ -325,7 +327,7 @@ internal static bool TryGetOpenApiValue(IOpenApiAny openApiAny, FormulaType form } else if (openApiAny is IDictionary o) { - Dictionary dvParams = new (); + Dictionary dvParams = new(); foreach (KeyValuePair kvp in o) { @@ -676,17 +678,13 @@ internal static ConnectorType GetConnectorType(this ISwaggerParameter openApiPar return new ConnectorType(schema, openApiParameter, FormulaType.String, hiddenfields.ToRecordType()); } - //ConnectorType propertyType = new OpenApiParameter() { Name = propLogicalName, Required = schema.Required.Contains(propLogicalName), Schema = kv.Value, Extensions = kv.Value.Extensions }.GetConnectorType(settings.Stack(schemaIdentifier)); ConnectorType propertyType = new SwaggerParameter(propLogicalName, schema.Required.Contains(propLogicalName), kv.Value, kv.Value.Extensions).GetConnectorType(settings.Stack(schemaIdentifier)); - if (settings.SqlRelationships != null) - { - SqlRelationship relationship = settings.SqlRelationships.FirstOrDefault(sr => sr.ColumnName == propLogicalName); + IEnumerable relationships = settings.SqlRelationships?.Where(sr => sr.ColumnName == propLogicalName); - if (relationship != null) - { - propertyType.SetRelationship(relationship); - } + if (relationships?.Any() == true) + { + propertyType.SetRelationships(relationships); } settings.UnStack(); @@ -818,7 +816,7 @@ public static HttpMethod ToHttpMethod(this OperationType key) } public static FormulaType GetReturnType(this OpenApiOperation openApiOperation, ConnectorCompatibility compatibility) - { + { ConnectorType connectorType = openApiOperation.GetConnectorReturnType(compatibility); FormulaType ft = connectorType.HasErrors ? ConnectorType.DefaultType : connectorType?.FormulaType ?? new BlankType(); return ft; @@ -890,7 +888,7 @@ internal static ConnectorType GetConnectorReturnType(this OpenApiOperation openA /// When we cannot determine the content type to use. public static (string ContentType, OpenApiMediaType MediaType) GetContentTypeAndSchema(this IDictionary content) { - Dictionary list = new (); + Dictionary list = new(); foreach (var ct in _knownContentTypes) { @@ -1045,7 +1043,7 @@ internal static ConnectorDynamicValue GetDynamicValue(this ISwaggerExtensions pa { // Parameters is required in the spec but there are examples where it's not specified and we'll support this condition with an empty list IDictionary op_prms = apiObj.TryGetValue("parameters", out IOpenApiAny openApiAny) && openApiAny is IDictionary apiString ? apiString : null; - ConnectorDynamicValue cdv = new (op_prms); + ConnectorDynamicValue cdv = new(op_prms); // Mandatory operationId for connectors, except when capibility or builtInOperation are defined apiObj.WhenPresent("operationId", (opId) => cdv.OperationId = OpenApiHelperFunctions.NormalizeOperationId(opId)); @@ -1074,7 +1072,7 @@ internal static ConnectorDynamicList GetDynamicList(this ISwaggerExtensions para { // Parameters is required in the spec but there are examples where it's not specified and we'll support this condition with an empty list IDictionary op_prms = apiObj.TryGetValue("parameters", out IOpenApiAny openApiAny) && openApiAny is IDictionary apiString ? apiString : null; - ConnectorDynamicList cdl = new (op_prms) + ConnectorDynamicList cdl = new(op_prms) { OperationId = OpenApiHelperFunctions.NormalizeOperationId(opId.Value), }; @@ -1107,7 +1105,7 @@ internal static ConnectorDynamicList GetDynamicList(this ISwaggerExtensions para internal static Dictionary GetParameterMap(this IDictionary opPrms, SupportsConnectorErrors errors, bool forceString = false) { - Dictionary dvParams = new (); + Dictionary dvParams = new(); if (opPrms == null) { @@ -1204,7 +1202,7 @@ internal static ConnectorDynamicSchema GetDynamicSchema(this ISwaggerExtensions // Parameters is required in the spec but there are examples where it's not specified and we'll support this condition with an empty list IDictionary op_prms = apiObj.TryGetValue("parameters", out IOpenApiAny openApiAny) && openApiAny is IDictionary apiString ? apiString : null; - ConnectorDynamicSchema cds = new (op_prms) + ConnectorDynamicSchema cds = new(op_prms) { OperationId = OpenApiHelperFunctions.NormalizeOperationId(opId.Value), }; @@ -1236,7 +1234,7 @@ internal static ConnectorDynamicProperty GetDynamicProperty(this ISwaggerExtensi // Parameters is required in the spec but there are examples where it's not specified and we'll support this condition with an empty list IDictionary op_prms = apiObj.TryGetValue("parameters", out IOpenApiAny openApiAny) && openApiAny is IDictionary apiString ? apiString : null; - ConnectorDynamicProperty cdp = new (op_prms) + ConnectorDynamicProperty cdp = new(op_prms) { OperationId = OpenApiHelperFunctions.NormalizeOperationId(opId.Value), }; diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpExtensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpExtensions.cs index 545a87cd21..be8368a2e8 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpExtensions.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpExtensions.cs @@ -1,22 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Collections.Generic; +using System.Linq; using Microsoft.PowerFx.Types; namespace Microsoft.PowerFx.Connectors { public static class CdpExtensions { - public static bool TryGetFieldExternalTableName(this RecordType recordType, string fieldName, out string tableName, out string foreignKey) + public static bool TryGetFieldRelationships(this RecordType recordType, string fieldName, out IEnumerable relationships) { - if (recordType is not CdpRecordType cdpRecordType) + if (recordType is not CdpRecordType cdpRecordType || (!cdpRecordType.TryGetFieldRelationships(fieldName, out relationships) && relationships.Any())) { - tableName = null; - foreignKey = null; + relationships = null; return false; - } + } - return cdpRecordType.TryGetFieldExternalTableName(fieldName, out tableName, out foreignKey); + return true; } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpRecordType.cs b/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpRecordType.cs index 47c6e565aa..ddd73f90d6 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpRecordType.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Public/CdpRecordType.cs @@ -23,20 +23,17 @@ internal CdpRecordType(ConnectorType connectorType, ICdpTableResolver tableResol TableResolver = tableResolver; } - public bool TryGetFieldExternalTableName(string fieldName, out string tableName, out string foreignKey) + public bool TryGetFieldRelationships(string fieldName, out IEnumerable relationships) { - tableName = null; - foreignKey = null; - ConnectorType connectorType = ConnectorType.Fields.First(ct => ct.Name == fieldName); if (connectorType == null || connectorType.ExternalTables?.Any() != true) { + relationships = null; return false; } - tableName = connectorType.ExternalTables.First(); - foreignKey = connectorType.ForeignKey; + relationships = connectorType.ExternalTables; return true; } @@ -60,7 +57,8 @@ private bool TryGetFieldType(string fieldName, bool ignorelationship, out Formul return true; } - string tableName = field.ExternalTables.First(); + // $$$ We should NOT call First() here but consider all possible foreign tables + string tableName = field.ExternalTables.First().ForeignTable; try { diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorRelationship.cs b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorRelationship.cs new file mode 100644 index 0000000000..2f08c42e93 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorRelationship.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx.Core.Utils; + +namespace Microsoft.PowerFx.Connectors +{ + public class ConnectorRelationship + { + public string ForeignTable { get; internal init; } + + public string ForeignKey { get; internal init; } + + public override int GetHashCode() + { + return Hashing.CombineHash(ForeignTable?.GetHashCode() ?? 0, ForeignKey?.GetHashCode() ?? 0); + } + + public override string ToString() => $"{ForeignTable ?? ""}:{ForeignKey ?? ""}"; + } +} diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorType.cs b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorType.cs index b896b3d9da..52213ed5f5 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorType.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Public/ConnectorType.cs @@ -20,7 +20,7 @@ namespace Microsoft.PowerFx.Connectors // FormulaType is used to represent the type of the parameter in the Power Fx expression as used in Power Apps // ConnectorType contains more details information coming from the swagger file and extensions [DebuggerDisplay("{FormulaType._type}")] - public class ConnectorType : SupportsConnectorErrors + public class ConnectorType : SupportsConnectorErrors, IEquatable { // "name" public string Name { get; internal set; } @@ -112,11 +112,9 @@ public class ConnectorType : SupportsConnectorErrors internal ISwaggerSchema Schema { get; private set; } = null; // Relationships to external tables - internal List ExternalTables { get; set; } + internal IEnumerable ExternalTables => _externalTables; - internal string RelationshipName { get; set; } - - internal string ForeignKey { get; set; } + private List _externalTables; internal ConnectorType(ISwaggerSchema schema, ISwaggerParameter openApiParameter, FormulaType formulaType, ErrorResourceKey warning = default) { @@ -148,9 +146,11 @@ internal ConnectorType(ISwaggerSchema schema, ISwaggerParameter openApiParameter // SalesForce only if (schema.ReferenceTo != null && schema.ReferenceTo.Count == 1) { - ExternalTables = new List(schema.ReferenceTo); - RelationshipName = schema.RelationshipName; - ForeignKey = null; // SalesForce doesn't provide it, defaults to "Id" + _externalTables = schema.ReferenceTo.Select(r => new ConnectorRelationship() + { + ForeignTable = r, + ForeignKey = null // Seems to always be Id + }).ToList(); } Fields = Array.Empty(); @@ -290,12 +290,18 @@ internal DisplayNameProvider DisplayNameProvider private DisplayNameProvider _displayNameProvider; - internal void SetRelationship(SqlRelationship relationship) + internal void SetRelationships(IEnumerable relationships) { - ExternalTables ??= new List(); - ExternalTables.Add(relationship.ReferencedTable); - RelationshipName = relationship.RelationshipName; - ForeignKey = relationship.ReferencedColumnName; + _externalTables ??= new List(); + + foreach (SqlRelationship relationship in relationships) + { + _externalTables.Add(new ConnectorRelationship() + { + ForeignTable = relationship.ReferencedTable, + ForeignKey = relationship.ReferencedColumnName + }); + } } private void AggregateErrors(ConnectorType[] types) @@ -336,5 +342,92 @@ private Dictionary GetEnum() return enumDisplayNames.Zip(enumValues, (dn, ev) => new KeyValuePair(dn, ev)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } + + public bool Equals(ConnectorType other) + { + if (other == null) + { + return false; + } + + return this.Name == other.Name && + this.DisplayName == other.DisplayName && + this.Description == other.Description && + this.IsRequired == other.IsRequired && + Enumerable.SequenceEqual((IList)this.Fields ?? Array.Empty(), (IList)other.Fields ?? Array.Empty()) && + Enumerable.SequenceEqual((IList)this.HiddenFields ?? Array.Empty(), (IList)other.HiddenFields ?? Array.Empty()) && + this.ExplicitInput == other.ExplicitInput && + this.IsEnum == other.IsEnum && + Enumerable.SequenceEqual((IList)this.EnumValues ?? Array.Empty(), (IList)other.EnumValues ?? Array.Empty()) && + Enumerable.SequenceEqual((IList)this.EnumDisplayNames ?? Array.Empty(), (IList)other.EnumDisplayNames ?? Array.Empty()) && + this.Visibility == other.Visibility && + ((this.Capabilities == null && other.Capabilities == null) || this.Capabilities.Equals(other.Capabilities)) && + this.KeyType == other.KeyType && + this.KeyOrder == other.KeyOrder && + this.Permission == other.Permission && + this.NotificationUrl == other.NotificationUrl && + ((this.HiddenRecordType == null && other.HiddenRecordType == null) || this.HiddenRecordType.Equals(other.HiddenRecordType)) && + this.Binary == other.Binary && + this.MediaKind == other.MediaKind && + Enumerable.SequenceEqual((IList)this.ExternalTables ?? Array.Empty(), (IList)other.ExternalTables ?? Array.Empty()); + } + + public override bool Equals(object obj) + { + return Equals(obj as ConnectorType); + } + + public override int GetHashCode() + { + int h = Hashing.CombineHash(Name.GetHashCode(), DisplayName?.GetHashCode() ?? 0, Description?.GetHashCode() ?? 0, IsRequired.GetHashCode()); + + if (Fields != null) + { + foreach (ConnectorType field in Fields) + { + h = Hashing.CombineHash(h, field.GetHashCode()); + } + } + + if (HiddenFields != null) + { + foreach (ConnectorType hiddenField in HiddenFields) + { + h = Hashing.CombineHash(h, hiddenField.GetHashCode()); + } + } + + h = Hashing.CombineHash(h, ExplicitInput.GetHashCode(), IsEnum.GetHashCode(), Visibility.GetHashCode()); + + if (EnumValues != null) + { + foreach (FormulaValue enumValue in EnumValues) + { + h = Hashing.CombineHash(h, enumValue.GetHashCode()); + } + } + + if (EnumDisplayNames != null) + { + foreach (string enumDisplayName in EnumDisplayNames) + { + h = Hashing.CombineHash(h, enumDisplayName.GetHashCode()); + } + } + + h = Hashing.CombineHash(h, Capabilities.GetHashCode(), KeyType.GetHashCode(), KeyOrder.GetHashCode(), Permission.GetHashCode(), NotificationUrl?.GetHashCode() ?? 0, HiddenRecordType.GetHashCode()); + h = Hashing.CombineHash(h, Binary.GetHashCode()); + h = Hashing.CombineHash(h, MediaKind.GetHashCode()); + + if (ExternalTables != null) + { + foreach (ConnectorRelationship relationship in ExternalTables) + { + h = Hashing.CombineHash(h, relationship.GetHashCode()); + } + } + + return h; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilities.cs b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilities.cs index 79656e7f00..91f5f46168 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilities.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilities.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -12,7 +13,7 @@ namespace Microsoft.PowerFx.Connectors { - internal sealed class ColumnCapabilities : ColumnCapabilitiesBase, IColumnsCapabilities + internal sealed class ColumnCapabilities : ColumnCapabilitiesBase, IColumnsCapabilities, IEquatable { [JsonInclude] [JsonPropertyName(CapabilityConstants.ColumnProperties)] @@ -72,5 +73,32 @@ public static ColumnCapabilities ParseColumnCapabilities(IDictionary>)this._childColumnsCapabilities ?? Array.Empty>(), (IEnumerable>)other._childColumnsCapabilities ?? Array.Empty>()) && + this.Capabilities.Equals(other.Capabilities); + } + + public override bool Equals(object obj) + { + return Equals(obj as ColumnCapabilities); + } + + public override int GetHashCode() + { + int h = Capabilities.GetHashCode(); + + if (_childColumnsCapabilities != null) + { + foreach (KeyValuePair kvp in _childColumnsCapabilities) + { + h = Hashing.CombineHash(h, kvp.Key.GetHashCode()); + h = Hashing.CombineHash(h, kvp.Value.GetHashCode()); + } + } + + return h; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesBase.cs b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesBase.cs index 9b60851193..d020f56164 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesBase.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesBase.cs @@ -8,5 +8,6 @@ namespace Microsoft.PowerFx.Connectors [JsonConverter(typeof(AbstractTypeConverter))] internal abstract class ColumnCapabilitiesBase { + public abstract override int GetHashCode(); } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesDefinition.cs b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesDefinition.cs index e733d84476..33832cab0b 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesDefinition.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ColumnCapabilitiesDefinition.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; using Microsoft.PowerFx.Core.Utils; @@ -10,7 +12,7 @@ namespace Microsoft.PowerFx.Connectors { - internal sealed class ColumnCapabilitiesDefinition + internal sealed class ColumnCapabilitiesDefinition : IEquatable { [JsonInclude] [JsonPropertyName(CapabilityConstants.FilterFunctions)] @@ -33,5 +35,37 @@ public ColumnCapabilitiesDefinition(IEnumerable filterFunction) // ex: lt, le, eq, ne, gt, ge, and, or, not, contains, startswith, endswith, countdistinct, day, month, year, time FilterFunctions = filterFunction; } + + public bool Equals(ColumnCapabilitiesDefinition other) + { + if (other == null) + { + return false; + } + + return Enumerable.SequenceEqual(this.FilterFunctions ?? Array.Empty(), other.FilterFunctions ?? Array.Empty()) && + this.QueryAlias == other.QueryAlias && + this.IsChoice == other.IsChoice; + } + + public override bool Equals(object obj) + { + return Equals(obj as ColumnCapabilitiesDefinition); + } + + public override int GetHashCode() + { + int h = Hashing.CombineHash(QueryAlias.GetHashCode(), IsChoice?.GetHashCode() ?? 0); + + if (FilterFunctions != null) + { + foreach (var filterFunction in FilterFunctions) + { + h = Hashing.CombineHash(h, filterFunction.GetHashCode()); + } + } + + return h; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ComplexColumnCapabilities.cs b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ComplexColumnCapabilities.cs index 7b6d1d20ea..64a44c3e56 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ComplexColumnCapabilities.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ComplexColumnCapabilities.cs @@ -25,5 +25,10 @@ public void AddColumnCapability(string name, ColumnCapabilitiesBase capability) _childColumnsCapabilities.Add(name, capability); } + + public override int GetHashCode() + { + throw new System.NotImplementedException(); + } } } diff --git a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ServiceCapabilities.cs b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ServiceCapabilities.cs index f81b983e58..b3d0ae8c51 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ServiceCapabilities.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/Tabular/Capabilities/ServiceCapabilities.cs @@ -172,7 +172,8 @@ public static TableDelegationInfo ToDelegationInfo(ServiceCapabilities serviceCa _ => throw new NotImplementedException() }); - Dictionary columnWithRelationships = connectorType.Fields.Where(f => f.ExternalTables?.Any() == true).Select(f => (f.Name, f.ExternalTables.First())).ToDictionary(tpl => tpl.Name, tpl => tpl.Item2); + // $$$ We should not use ExternalTables.First() but consider all relationships here + Dictionary columnWithRelationships = connectorType.Fields.Where(f => f.ExternalTables?.Any() == true).Select(f => (f.Name, f.ExternalTables.First().ForeignTable)).ToDictionary(tpl => tpl.Name, tpl => tpl.ForeignTable); return new CdpDelegationInfo() { diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CompileTimeTypeWrapperRecordValue.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CompileTimeTypeWrapperRecordValue.cs index 6092c6ff44..83374c820e 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Values/CompileTimeTypeWrapperRecordValue.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Values/CompileTimeTypeWrapperRecordValue.cs @@ -44,10 +44,10 @@ public override bool TryShallowCopy(out FormulaValue copy) protected override bool TryGetField(FormulaType fieldType, string fieldName, out FormulaValue result) { if (Type.TryGetFieldType(fieldName, out var compileTimeType)) - { + { // Only return field which were specified via the expectedType (IE RecordType), // because inner record value may have more fields than the expected type. - if (compileTimeType == fieldType && _fields.TryGetValue(fieldName, out result)) + if (compileTimeType.Equals(fieldType) && _fields.TryGetValue(fieldName, out result)) { return true; } diff --git a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformTabularTests.cs b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformTabularTests.cs index f77dbd7a9d..60bd700776 100644 --- a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformTabularTests.cs +++ b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PowerPlatformTabularTests.cs @@ -237,10 +237,11 @@ public async Task SQL_CdpTabular_GetTables2() StringValue address = Assert.IsType(result); Assert.Equal("HL Road Frame - Black, 58", address.Value); - bool b = sqlTable.RecordType.TryGetFieldExternalTableName("ProductModelID", out string externalTableName, out string foreignKey); + bool b = sqlTable.RecordType.TryGetFieldRelationships("ProductModelID", out IEnumerable relationships); Assert.True(b); - Assert.Equal("[SalesLT].[ProductModel]", externalTableName); // Logical Name - Assert.Equal("ProductModelID", foreignKey); + Assert.Single(relationships); + Assert.Equal("[SalesLT].[ProductModel]", relationships.First().ForeignTable); // Logical Name + Assert.Equal("ProductModelID", relationships.First().ForeignKey); testConnector.SetResponseFromFiles(@"Responses\SQL GetSchema ProductModel.json", @"Responses\SQL GetRelationships SampleDB.json"); b = sqlTable.RecordType.TryGetFieldType("ProductModelID", out FormulaType productModelID); @@ -643,22 +644,24 @@ public async Task SF_CdpTabular_GetTables() Assert.True(sfTable._tabularService.IsInitialized); Assert.True(sfTable.IsDelegable); - // Note relationships with external tables (logicalName`displayName[externalTable]:type) - // CreatedById`'Created By ID'[User]:s - // LastModifiedById`'Last Modified By ID'[User]:s - // Modified By ID'[User]:s - // MasterRecordId`'Master Record ID'[Account]:s - // OwnerId`'Owner ID'[User]:s - // ParentId`'Parent Account ID'[Account]:s + // Note relationships with external tables (logicalName`displayName[externalTable:foreignKey)]:type) + // In this particular case, foreignKey is always null with SalesForce + // CreatedById`'Created By ID'[User:]:s + // LastModifiedById`'Last Modified By ID'[User:]:s + // Modified By ID'[User:]:s + // MasterRecordId`'Master Record ID'[Account,:]:s + // OwnerId`'Owner ID'[User,:]:s + // ParentId`'Parent Account ID'[Account,:]:s // Note 2: ~ notation denotes a relationship. Ex: fieldname`displayname:~externaltable:type Assert.Equal( - "r![AccountSource`'Account Source':l, BillingCity`'Billing City':s, BillingCountry`'Billing Country':s, BillingGeocodeAccuracy`'Billing Geocode Accuracy':l, BillingLatitude`'Billing Latitude':w, BillingLongitude`'Billing " + - "Longitude':w, BillingPostalCode`'Billing Zip/Postal Code':s, BillingState`'Billing State/Province':s, BillingStreet`'Billing Street':s, CreatedById`'Created By ID'[User]:~User:s, CreatedDate`'Created Date':d, " + - "Description`'Account Description':s, Id`'Account ID':s, Industry:l, IsDeleted`Deleted:b, Jigsaw`'Data.com Key':s, JigsawCompanyId`'Jigsaw Company ID':s, LastActivityDate`'Last Activity':D, LastModifiedById`'Last " + - "Modified By ID'[User]:~User:s, LastModifiedDate`'Last Modified Date':d, LastReferencedDate`'Last Referenced Date':d, LastViewedDate`'Last Viewed Date':d, MasterRecordId`'Master Record ID'[Account]:~Account:s, " + - "Name`'Account Name':s, NumberOfEmployees`Employees:w, OwnerId`'Owner ID'[User]:~User:s, ParentId`'Parent Account ID'[Account]:~Account:s, Phone`'Account Phone':s, PhotoUrl`'Photo URL':s, ShippingCity`'Shipping " + - "City':s, ShippingCountry`'Shipping Country':s, ShippingGeocodeAccuracy`'Shipping Geocode Accuracy':l, ShippingLatitude`'Shipping Latitude':w, ShippingLongitude`'Shipping Longitude':w, ShippingPostalCode`'Shipping " + - "Zip/Postal Code':s, ShippingState`'Shipping State/Province':s, ShippingStreet`'Shipping Street':s, SicDesc`'SIC Description':s, SystemModstamp`'System Modstamp':d, Type`'Account Type':l, Website:s]", + "r![AccountSource`'Account Source':l, BillingCity`'Billing City':s, BillingCountry`'Billing Country':s, BillingGeocodeAccuracy`'Billing Geocode Accuracy':l, BillingLatitude`'Billing " + + "Latitude':w, BillingLongitude`'Billing Longitude':w, BillingPostalCode`'Billing Zip/Postal Code':s, BillingState`'Billing State/Province':s, BillingStreet`'Billing Street':s, CreatedById`'Created " + + "By ID'[User:]:~User:s, CreatedDate`'Created Date':d, Description`'Account Description':s, Id`'Account ID':s, Industry:l, IsDeleted`Deleted:b, Jigsaw`'Data.com Key':s, JigsawCompanyId`'Jigsaw " + + "Company ID':s, LastActivityDate`'Last Activity':D, LastModifiedById`'Last Modified By ID'[User:]:~User:s, LastModifiedDate`'Last Modified Date':d, LastReferencedDate`'Last Referenced " + + "Date':d, LastViewedDate`'Last Viewed Date':d, MasterRecordId`'Master Record ID'[Account:]:~Account:s, Name`'Account Name':s, NumberOfEmployees`Employees:w, OwnerId`'Owner ID'[User:" + + "]:~User:s, ParentId`'Parent Account ID'[Account:]:~Account:s, Phone`'Account Phone':s, PhotoUrl`'Photo URL':s, ShippingCity`'Shipping City':s, ShippingCountry`'Shipping Country':s, " + + "ShippingGeocodeAccuracy`'Shipping Geocode Accuracy':l, ShippingLatitude`'Shipping Latitude':w, ShippingLongitude`'Shipping Longitude':w, ShippingPostalCode`'Shipping Zip/Postal Code':s, " + + "ShippingState`'Shipping State/Province':s, ShippingStreet`'Shipping Street':s, SicDesc`'SIC Description':s, SystemModstamp`'System Modstamp':d, Type`'Account Type':l, Website:s]", ((CdpRecordType)sfTable.RecordType).ToStringWithDisplayNames()); Assert.Equal("Account", sfTable.RecordType.TableSymbolName); @@ -702,10 +705,11 @@ public async Task SF_CdpTabular_GetTables() // needs Microsoft.PowerFx.Connectors.CdpExtensions // this call does not make any network call - bool b = sfTable.RecordType.TryGetFieldExternalTableName("OwnerId", out string externalTableName, out string foreignKey); + bool b = sfTable.RecordType.TryGetFieldRelationships("OwnerId", out IEnumerable relationships); Assert.True(b); - Assert.Equal("User", externalTableName); - Assert.Null(foreignKey); // Always the case with SalesForce + Assert.Single(relationships); + Assert.Equal("User", relationships.First().ForeignTable); + Assert.Null(relationships.First().ForeignKey); // Always the case with SalesForce testConnector.SetResponseFromFile(@"Responses\SF GetSchema Users.json"); b = sfTable.RecordType.TryGetFieldType("OwnerId", out FormulaType ownerIdType); @@ -720,24 +724,24 @@ public async Task SF_CdpTabular_GetTables() Assert.Equal("User", userTable.TableSymbolName); Assert.Equal( - "r![AboutMe`'About Me':s, AccountId`'Account ID'[Account]:~Account:s, Alias:s, BadgeText`'User Photo badge text overlay':s, BannerPhotoUrl`'Url for banner photo':s, CallCenterId`'Call " + - "Center ID':s, City:s, CommunityNickname`Nickname:s, CompanyName`'Company Name':s, ContactId`'Contact ID'[Contact]:~Contact:s, Country:s, CreatedById`'Created By ID'[User]:~User:s, CreatedDate`'Created " + - "Date':d, DefaultGroupNotificationFrequency`'Default Notification Frequency when Joining Groups':l, DelegatedApproverId`'Delegated Approver ID':s, Department:s, DigestFrequency`'Chatter " + - "Email Highlights Frequency':l, Division:s, Email:s, EmailEncodingKey`'Email Encoding':l, EmailPreferencesAutoBcc`AutoBcc:b, EmailPreferencesAutoBccStayInTouch`AutoBccStayInTouch:b, " + - "EmailPreferencesStayInTouchReminder`StayInTouchReminder:b, EmployeeNumber`'Employee Number':s, Extension:s, Fax:s, FederationIdentifier`'SAML Federation ID':s, FirstName`'First Name':s, " + - "ForecastEnabled`'Allow Forecasting':b, FullPhotoUrl`'Url for full-sized Photo':s, GeocodeAccuracy`'Geocode Accuracy':l, Id`'User ID':s, IsActive`Active:b, IsExtIndicatorVisible`'Show " + - "external indicator':b, IsProfilePhotoActive`'Has Profile Photo':b, LanguageLocaleKey`Language:l, LastLoginDate`'Last Login':d, LastModifiedById`'Last Modified By ID'[User]:~User:s, " + + "r![AboutMe`'About Me':s, AccountId`'Account ID'[Account:]:~Account:s, Alias:s, BadgeText`'User Photo badge text overlay':s, BannerPhotoUrl`'Url for banner photo':s, CallCenterId`'Call " + + "Center ID':s, City:s, CommunityNickname`Nickname:s, CompanyName`'Company Name':s, ContactId`'Contact ID'[Contact:]:~Contact:s, Country:s, CreatedById`'Created By ID'[User:]:~User:s, " + + "CreatedDate`'Created Date':d, DefaultGroupNotificationFrequency`'Default Notification Frequency when Joining Groups':l, DelegatedApproverId`'Delegated Approver ID':s, Department:s, " + + "DigestFrequency`'Chatter Email Highlights Frequency':l, Division:s, Email:s, EmailEncodingKey`'Email Encoding':l, EmailPreferencesAutoBcc`AutoBcc:b, EmailPreferencesAutoBccStayInTouch`AutoBccStayInTouc" + + "h:b, EmailPreferencesStayInTouchReminder`StayInTouchReminder:b, EmployeeNumber`'Employee Number':s, Extension:s, Fax:s, FederationIdentifier`'SAML Federation ID':s, FirstName`'First " + + "Name':s, ForecastEnabled`'Allow Forecasting':b, FullPhotoUrl`'Url for full-sized Photo':s, GeocodeAccuracy`'Geocode Accuracy':l, Id`'User ID':s, IsActive`Active:b, IsExtIndicatorVisible`'Show " + + "external indicator':b, IsProfilePhotoActive`'Has Profile Photo':b, LanguageLocaleKey`Language:l, LastLoginDate`'Last Login':d, LastModifiedById`'Last Modified By ID'[User:]:~User:s, " + "LastModifiedDate`'Last Modified Date':d, LastName`'Last Name':s, LastPasswordChangeDate`'Last Password Change or Reset':d, LastReferencedDate`'Last Referenced Date':d, LastViewedDate`'Last " + - "Viewed Date':d, Latitude:w, LocaleSidKey`Locale:l, Longitude:w, ManagerId`'Manager ID'[User]:~User:s, MediumBannerPhotoUrl`'Url for Android banner photo':s, MediumPhotoUrl`'Url for " + - "medium profile photo':s, MiddleName`'Middle Name':s, MobilePhone`Mobile:s, Name`'Full Name':s, OfflinePdaTrialExpirationDate`'Sales Anywhere Trial Expiration Date':d, OfflineTrialExpirationDate`'Offlin" + - "e Edition Trial Expiration Date':d, OutOfOfficeMessage`'Out of office message':s, Phone:s, PostalCode`'Zip/Postal Code':s, ProfileId`'Profile ID'[Profile]:~Profile:s, ReceivesAdminInfoEmails`'Admin " + - "Info Emails':b, ReceivesInfoEmails`'Info Emails':b, SenderEmail`'Email Sender Address':s, SenderName`'Email Sender Name':s, Signature`'Email Signature':s, SmallBannerPhotoUrl`'Url for " + - "IOS banner photo':s, SmallPhotoUrl`Photo:s, State`'State/Province':s, StayInTouchNote`'Stay-in-Touch Email Note':s, StayInTouchSignature`'Stay-in-Touch Email Signature':s, StayInTouchSubject`'Stay-in-T" + - "ouch Email Subject':s, Street:s, Suffix:s, SystemModstamp`'System Modstamp':d, TimeZoneSidKey`'Time Zone':l, Title:s, UserPermissionsAvantgoUser`'AvantGo User':b, UserPermissionsCallCenterAutoLogin`'Au" + - "to-login To Call Center':b, UserPermissionsInteractionUser`'Flow User':b, UserPermissionsKnowledgeUser`'Knowledge User':b, UserPermissionsLiveAgentUser`'Chat User':b, UserPermissionsMarketingUser`'Mark" + - "eting User':b, UserPermissionsMobileUser`'Apex Mobile User':b, UserPermissionsOfflineUser`'Offline User':b, UserPermissionsSFContentUser`'Salesforce CRM Content User':b, UserPermissionsSupportUser`'Ser" + - "vice Cloud User':b, UserPreferencesActivityRemindersPopup`ActivityRemindersPopup:b, UserPreferencesApexPagesDeveloperMode`ApexPagesDeveloperMode:b, UserPreferencesCacheDiagnostics`CacheDiagnostics:b, " + - "UserPreferencesCreateLEXAppsWTShown`CreateLEXAppsWTShown:b, UserPreferencesDisCommentAfterLikeEmail`DisCommentAfterLikeEmail:b, UserPreferencesDisMentionsCommentEmail`DisMentionsCommentEmail:b, " + + "Viewed Date':d, Latitude:w, LocaleSidKey`Locale:l, Longitude:w, ManagerId`'Manager ID'[User:]:~User:s, MediumBannerPhotoUrl`'Url for Android banner photo':s, MediumPhotoUrl`'Url " + + "for medium profile photo':s, MiddleName`'Middle Name':s, MobilePhone`Mobile:s, Name`'Full Name':s, OfflinePdaTrialExpirationDate`'Sales Anywhere Trial Expiration Date':d, OfflineTrialExpirationDate`'Of" + + "fline Edition Trial Expiration Date':d, OutOfOfficeMessage`'Out of office message':s, Phone:s, PostalCode`'Zip/Postal Code':s, ProfileId`'Profile ID'[Profile:]:~Profile:s, ReceivesAdminInfoEmails`'A" + + "dmin Info Emails':b, ReceivesInfoEmails`'Info Emails':b, SenderEmail`'Email Sender Address':s, SenderName`'Email Sender Name':s, Signature`'Email Signature':s, SmallBannerPhotoUrl`'Url " + + "for IOS banner photo':s, SmallPhotoUrl`Photo:s, State`'State/Province':s, StayInTouchNote`'Stay-in-Touch Email Note':s, StayInTouchSignature`'Stay-in-Touch Email Signature':s, StayInTouchSubject`'Stay-" + + "in-Touch Email Subject':s, Street:s, Suffix:s, SystemModstamp`'System Modstamp':d, TimeZoneSidKey`'Time Zone':l, Title:s, UserPermissionsAvantgoUser`'AvantGo User':b, UserPermissionsCallCenterAutoLogin" + + "`'Auto-login To Call Center':b, UserPermissionsInteractionUser`'Flow User':b, UserPermissionsKnowledgeUser`'Knowledge User':b, UserPermissionsLiveAgentUser`'Chat User':b, UserPermissionsMarketingUser`'" + + "Marketing User':b, UserPermissionsMobileUser`'Apex Mobile User':b, UserPermissionsOfflineUser`'Offline User':b, UserPermissionsSFContentUser`'Salesforce CRM Content User':b, UserPermissionsSupportUser`" + + "'Service Cloud User':b, UserPreferencesActivityRemindersPopup`ActivityRemindersPopup:b, UserPreferencesApexPagesDeveloperMode`ApexPagesDeveloperMode:b, UserPreferencesCacheDiagnostics`CacheDiagnostics:" + + "b, UserPreferencesCreateLEXAppsWTShown`CreateLEXAppsWTShown:b, UserPreferencesDisCommentAfterLikeEmail`DisCommentAfterLikeEmail:b, UserPreferencesDisMentionsCommentEmail`DisMentionsCommentEmail:b, " + "UserPreferencesDisProfPostCommentEmail`DisProfPostCommentEmail:b, UserPreferencesDisableAllFeedsEmail`DisableAllFeedsEmail:b, UserPreferencesDisableBookmarkEmail`DisableBookmarkEmail:b, " + "UserPreferencesDisableChangeCommentEmail`DisableChangeCommentEmail:b, UserPreferencesDisableEndorsementEmail`DisableEndorsementEmail:b, UserPreferencesDisableFileShareNotificationsForApi`DisableFileSha" + "reNotificationsForApi:b, UserPreferencesDisableFollowersEmail`DisableFollowersEmail:b, UserPreferencesDisableLaterCommentEmail`DisableLaterCommentEmail:b, UserPreferencesDisableLikeEmail`DisableLikeEma" + @@ -758,7 +762,7 @@ public async Task SF_CdpTabular_GetTables() "ToExternalUsers`ShowStateToExternalUsers:b, UserPreferencesShowStateToGuestUsers`ShowStateToGuestUsers:b, UserPreferencesShowStreetAddressToExternalUsers`ShowStreetAddressToExternalUsers:b, " + "UserPreferencesShowStreetAddressToGuestUsers`ShowStreetAddressToGuestUsers:b, UserPreferencesShowTitleToExternalUsers`ShowTitleToExternalUsers:b, UserPreferencesShowTitleToGuestUsers`ShowTitleToGuestUs" + "ers:b, UserPreferencesShowWorkPhoneToExternalUsers`ShowWorkPhoneToExternalUsers:b, UserPreferencesShowWorkPhoneToGuestUsers`ShowWorkPhoneToGuestUsers:b, UserPreferencesSortFeedByComment`SortFeedByComme" + - "nt:b, UserPreferencesTaskRemindersCheckboxDefault`TaskRemindersCheckboxDefault:b, UserRoleId`'Role ID'[UserRole]:~UserRole:s, UserType`'User Type':l, Username:s]", userTable.ToStringWithDisplayNames()); + "nt:b, UserPreferencesTaskRemindersCheckboxDefault`TaskRemindersCheckboxDefault:b, UserRoleId`'Role ID'[UserRole:]:~UserRole:s, UserType`'User Type':l, Username:s]", userTable.ToStringWithDisplayNames()); // Missing field b = sfTable.RecordType.TryGetFieldType("XYZ", out FormulaType xyzType); diff --git a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PublicSurfaceTests.cs b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PublicSurfaceTests.cs index 2fe79d38e4..105302e708 100644 --- a/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PublicSurfaceTests.cs +++ b/src/tests/Microsoft.PowerFx.Connectors.Tests.Shared/PublicSurfaceTests.cs @@ -43,6 +43,7 @@ public void PublicSurfaceTest_Connectors() "Microsoft.PowerFx.Connectors.ConnectorParameters", "Microsoft.PowerFx.Connectors.ConnectorParameterWithSuggestions", "Microsoft.PowerFx.Connectors.ConnectorPermission", + "Microsoft.PowerFx.Connectors.ConnectorRelationship", "Microsoft.PowerFx.Connectors.ConnectorSchema", "Microsoft.PowerFx.Connectors.ConnectorSettings", "Microsoft.PowerFx.Connectors.ConnectorType",