diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs index 45720828..711558a1 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs @@ -488,6 +488,15 @@ protected Dictionary> CompileColumnMappings(DataSou ); } + if (lookupAttr != null && lookupAttr.Targets.Any(t => dataSource.Metadata[t].DataProviderId == DataProviders.ElasticDataProvider) && mappings.TryGetValue(destAttributeName + "pid", out var partitionIdColumn)) + { + var partitionIdExpr = (Expression)Expression.Property(entityParam, typeof(Entity).GetCustomAttribute().MemberName, Expression.Constant(partitionIdColumn)); + partitionIdExpr = Expression.Convert(partitionIdExpr, schema.Schema[partitionIdColumn].Type.ToNetType(out _)); + partitionIdExpr = SqlTypeConverter.Convert(partitionIdExpr, schema.Schema[partitionIdColumn].Type, DataTypeHelpers.NVarChar(100, dataSource.DefaultCollation, CollationLabel.Implicit)); + partitionIdExpr = SqlTypeConverter.Convert(partitionIdExpr, typeof(string)); + convertedExpr = Expr.Call(() => CreateElasticEntityReference(Expr.Arg(), Expr.Arg(), Expr.Arg()), convertedExpr, partitionIdExpr, Expression.Constant(dataSource.Metadata)); + } + destType = typeof(EntityReference); } else @@ -556,6 +565,24 @@ private static string ObjectTypeCodeToLogicalName(SqlInt32 otc, IAttributeMetada return attributeMetadataCache[otc.Value].LogicalName; } + private static EntityReference CreateElasticEntityReference(EntityReference entityReference, string partitionId, IAttributeMetadataCache metadata) + { + if (entityReference == null) + return null; + + if (partitionId == null) + return entityReference; + + var meta = metadata[entityReference.LogicalName]; + var keys = new KeyAttributeCollection + { + [meta.PrimaryIdAttribute] = entityReference.Id, + ["partitionid"] = partitionId + }; + + return new EntityReference(entityReference.LogicalName, keys); + } + /// /// Provides values to include in log messages /// diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs index 5548ead6..20c91b44 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs @@ -1304,6 +1304,9 @@ private void AddSchemaAttribute(DataSource dataSource, ColumnList schema, Dictio if (lookup.Targets?.Length != 1 && lookup.AttributeType != AttributeTypeCode.PartyList) AddSchemaAttribute(schema, aliases, AddSuffix(fullName, "type"), (attrMetadata.LogicalName + "type").EscapeIdentifier(), DataTypeHelpers.NVarChar(MetadataExtensions.EntityLogicalNameMaxLength, dataSource.DefaultCollation, CollationLabel.Implicit), notNull); ; + + if (lookup.Targets != null && lookup.Targets.Any(logicalName => dataSource.Metadata[logicalName].DataProviderId == DataProviders.ElasticDataProvider)) + AddSchemaAttribute(schema, aliases, AddSuffix(fullName, "pid"), (attrMetadata.LogicalName + "pid").EscapeIdentifier(), DataTypeHelpers.NVarChar(100, dataSource.DefaultCollation, CollationLabel.Implicit), false); } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index 718e7c14..fd76e467 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -1410,6 +1410,7 @@ private InsertNode ConvertInsertSpecification(NamedTableReference target, IList< var attributes = metadata.Attributes.ToDictionary(attr => attr.LogicalName, StringComparer.OrdinalIgnoreCase); var attributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); var virtualTypeAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); + var virtualPidAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); var schema = sourceColumns == null ? null : ((IDataExecutionPlanNodeInternal)source).GetSchema(_nodeContext); // Check all target columns are valid for create @@ -1430,6 +1431,18 @@ attr is LookupAttributeMetadata lookupAttr && continue; } + // Could be a virtual ___pid attribute + if (colName.EndsWith("pid", StringComparison.OrdinalIgnoreCase) && + attributes.TryGetValue(colName.Substring(0, colName.Length - 3), out attr) && + attr is LookupAttributeMetadata elasticLookupAttr && + elasticLookupAttr.Targets.Any(t => dataSource.Metadata[t].DataProviderId == DataProviders.ElasticDataProvider)) + { + if (!virtualPidAttributes.Add(colName)) + throw new NotSupportedQueryFragmentException(Sql4CdsError.DuplicateInsertUpdateColumn(col)); + + continue; + } + if (!attributes.TryGetValue(colName, out attr)) throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidColumnName(col)); @@ -1521,6 +1534,11 @@ attr is LookupAttributeMetadata lookupAttr && targetName = colName; targetType = DataTypeHelpers.NVarChar(MetadataExtensions.EntityLogicalNameMaxLength, dataSource.DefaultCollation, CollationLabel.CoercibleDefault); } + else if (virtualPidAttributes.Contains(colName)) + { + targetName = colName; + targetType = DataTypeHelpers.NVarChar(100, dataSource.DefaultCollation, CollationLabel.CoercibleDefault); + } else { var attr = attributes[colName]; @@ -1579,12 +1597,23 @@ attr is LookupAttributeMetadata lookupAttr && (schema == null || (node.ColumnMappings[targetAttrName].ToColumnReference().GetType(GetExpressionContext(schema, _nodeContext), out var lookupType) != typeof(SqlEntityReference) && lookupType != DataTypeHelpers.ImplicitIntForNullLiteral))) { // Special case: not required for listmember.entityid - if (metadata.LogicalName == "listmember" && targetLookupAttribute.LogicalName == "entityid") - continue; + if (metadata.LogicalName != "listmember" || targetLookupAttribute.LogicalName != "entityid") + { + throw new NotSupportedQueryFragmentException(Sql4CdsError.ReadOnlyColumn(col)) + { + Suggestion = $"Inserting values into a polymorphic lookup field requires setting the associated type column as well\r\nAdd a value for the {targetLookupAttribute.LogicalName}type column and set it to one of the following values:\r\n{String.Join("\r\n", targetLookupAttribute.Targets.Select(t => $"* {t}"))}" + }; + } + } + // If the lookup references an elastic table we also need the pid column + if (targetLookupAttribute.Targets.Any(t => dataSource.Metadata[t].DataProviderId == DataProviders.ElasticDataProvider) && + !virtualPidAttributes.Contains(targetAttrName + "pid") && + (schema == null || (node.ColumnMappings[targetAttrName].ToColumnReference().GetType(GetExpressionContext(schema, _nodeContext), out var elasticLookupType) != null && elasticLookupType != DataTypeHelpers.ImplicitIntForNullLiteral))) + { throw new NotSupportedQueryFragmentException(Sql4CdsError.ReadOnlyColumn(col)) { - Suggestion = $"Inserting values into a polymorphic lookup field requires setting the associated type column as well\r\nAdd a value for the {targetLookupAttribute.LogicalName}type column and set it to one of the following values:\r\n{String.Join("\r\n", targetLookupAttribute.Targets.Select(t => $"* {t}"))}" + Suggestion = $"Inserting values into an elastic lookup field requires setting the associated pid column as well\r\nAdd a value for the {targetLookupAttribute.LogicalName}pid column" }; } } @@ -1601,6 +1630,18 @@ attr is LookupAttributeMetadata lookupAttr && }; } } + else if (virtualPidAttributes.Contains(targetAttrName)) + { + var idAttrName = targetAttrName.Substring(0, targetAttrName.Length - 3); + + if (!attributeNames.Contains(idAttrName)) + { + throw new NotSupportedQueryFragmentException(Sql4CdsError.ReadOnlyColumn(col)) + { + Suggestion = $"Inserting values into an elastic lookup field requires setting the associated ID column as well\r\nAdd a value for the {idAttrName} column" + }; + } + } } return node; @@ -1903,6 +1944,7 @@ private UpdateNode ConvertUpdateStatement(UpdateSpecification update, IList attr.LogicalName, StringComparer.OrdinalIgnoreCase); var attributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); var virtualTypeAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); + var virtualPidAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); var existingAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); var useStateTransitions = !hints.OfType().Any(h => h.Hints.Any(s => s.Value.Equals("DISABLE_STATE_TRANSITIONS", StringComparison.OrdinalIgnoreCase))); var stateTransitions = useStateTransitions ? StateTransitionLoader.LoadStateTransitions(targetMetadata) : null; @@ -1970,6 +2012,14 @@ attr is LookupAttributeMetadata lookupAttr && if (!virtualTypeAttributes.Add(targetAttrName)) throw new NotSupportedQueryFragmentException(Sql4CdsError.DuplicateInsertUpdateColumn(assignment.Column)); } + else if (targetAttrName.EndsWith("pid", StringComparison.OrdinalIgnoreCase) && + attributes.TryGetValue(targetAttrName.Substring(0, targetAttrName.Length - 3), out attr) && + attr is LookupAttributeMetadata elasticLookupAttr && + elasticLookupAttr.Targets.Any(t => dataSource.Metadata[t].DataProviderId == DataProviders.ElasticDataProvider)) + { + if (!virtualPidAttributes.Add(targetAttrName)) + throw new NotSupportedQueryFragmentException(Sql4CdsError.DuplicateInsertUpdateColumn(assignment.Column)); + } else { if (!attributes.TryGetValue(targetAttrName, out attr)) @@ -2068,7 +2118,7 @@ attr is LookupAttributeMetadata lookupAttr && var source = ConvertSelectStatement(selectStatement); // Add UPDATE - var updateNode = ConvertSetClause(update.SetClauses, existingAttributes, dataSource, source, targetLogicalName, targetAlias, attributeNames, virtualTypeAttributes, hints); + var updateNode = ConvertSetClause(update.SetClauses, existingAttributes, dataSource, source, targetLogicalName, targetAlias, attributeNames, virtualTypeAttributes, virtualPidAttributes, hints); updateNode.StateTransitions = stateTransitions; return updateNode; @@ -2096,7 +2146,7 @@ private void CopyDmlHintsToSelectStatement(IList hints, SelectSta } } - private UpdateNode ConvertSetClause(IList setClauses, HashSet existingAttributes, DataSource dataSource, IExecutionPlanNodeInternal node, string targetLogicalName, string targetAlias, HashSet attributeNames, HashSet virtualTypeAttributes, IList queryHints) + private UpdateNode ConvertSetClause(IList setClauses, HashSet existingAttributes, DataSource dataSource, IExecutionPlanNodeInternal node, string targetLogicalName, string targetAlias, HashSet attributeNames, HashSet virtualTypeAttributes, HashSet virtualPidAttributes, IList queryHints) { var targetMetadata = dataSource.Metadata[targetLogicalName]; var attributes = targetMetadata.Attributes.ToDictionary(attr => attr.LogicalName, StringComparer.OrdinalIgnoreCase); @@ -2223,6 +2273,17 @@ private UpdateNode ConvertSetClause(IList setClauses, HashSet Suggestion = $"Add a SET clause for the {targetLookupAttribute.LogicalName}type column and set it to one of the following values:\r\n{String.Join("\r\n", targetLookupAttribute.Targets.Select(t => $"* {t}"))}" }; } + + // If the lookup references an elastic table we also need the pid column + if (targetLookupAttribute.Targets.Any(t => dataSource.Metadata[t].DataProviderId == DataProviders.ElasticDataProvider) && + !virtualPidAttributes.Contains(targetAttrName + "pid") && + (!sourceTypes.TryGetValue(targetAttrName, out var elasticLookupType) || elasticLookupType != DataTypeHelpers.ImplicitIntForNullLiteral)) + { + throw new NotSupportedQueryFragmentException("Updating an elastic lookup field requires setting the associated pid column as well", assignment.Column) + { + Suggestion = $"Add a SET clause for the {targetLookupAttribute.LogicalName}pid column" + }; + } } else if (virtualTypeAttributes.Contains(targetAttrName)) { @@ -2239,6 +2300,18 @@ private UpdateNode ConvertSetClause(IList setClauses, HashSet }; } } + else if (virtualPidAttributes.Contains(targetAttrName)) + { + var idAttrName = targetAttrName.Substring(0, targetAttrName.Length - 3); + + if (!attributeNames.Contains(idAttrName)) + { + throw new NotSupportedQueryFragmentException("Updating an elastic lookup field requires setting the associated ID column as well", assignment.Column) + { + Suggestion = $"Add a SET clause for the {idAttrName} column" + }; + } + } } return update; diff --git a/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs b/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs index bb4f7d6b..0df9e48f 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs @@ -433,7 +433,7 @@ public IEnumerable GetSuggestions(string text, int pos) if (tables.TryGetValue(targetTable, out var tableName)) { if (TryParseTableName(tableName, out var instanceName, out _, out tableName) && _dataSources.TryGetValue(instanceName, out var instance) && instance.Metadata.TryGetMinimalData(tableName, out var metadata)) - return FilterList(metadata.Attributes.Where(a => a.IsValidForUpdate != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(a => a.IsValidForUpdate != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true, instance.Metadata)).OrderBy(a => a), currentWord); } } @@ -456,14 +456,14 @@ public IEnumerable GetSuggestions(string text, int pos) if (instance.Messages.TryGetValue(messageName, out var message) && message.IsValidAsTableValuedFunction()) { - return FilterList(GetMessageOutputAttributes(message, instance).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false)).OrderBy(a => a), currentWord); + return FilterList(GetMessageOutputAttributes(message, instance).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false, instance.Metadata)).OrderBy(a => a), currentWord); } } else { // Table if (instance.Metadata.TryGetMinimalData((schemaName == "metadata" ? "metadata." : "") + tableName, out var metadata)) - return FilterList(metadata.Attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false, instance.Metadata)).OrderBy(a => a), currentWord); } } } @@ -497,7 +497,7 @@ public IEnumerable GetSuggestions(string text, int pos) { attributeFilter = a => a.IsValidForCreate != false && a.AttributeOf == null; } - return FilterList(metadata.Attributes.Where(attributeFilter).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(attributeFilter).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true, instance.Metadata)).OrderBy(a => a), currentWord); } } else @@ -509,6 +509,7 @@ public IEnumerable GetSuggestions(string text, int pos) // * variables var items = new List(); var attributes = new List(); + var instance = default(AutocompleteDataSource); foreach (var table in tables) { @@ -518,7 +519,7 @@ public IEnumerable GetSuggestions(string text, int pos) var messageName = table.Value.Substring(0, table.Value.Length - 1); if (TryParseTableName(messageName, out var instanceName, out var schemaName, out var tableName) && - _dataSources.TryGetValue(instanceName, out var instance) && + _dataSources.TryGetValue(instanceName, out instance) && (string.IsNullOrEmpty(schemaName) || schemaName == "dbo") && instance.Messages.TryGetValue(messageName, out var message) && message.IsValidAsTableValuedFunction()) @@ -532,7 +533,7 @@ public IEnumerable GetSuggestions(string text, int pos) else { // Table - if (TryParseTableName(table.Value, out var instanceName, out var schemaName, out var tableName) && _dataSources.TryGetValue(instanceName, out var instance)) + if (TryParseTableName(table.Value, out var instanceName, out var schemaName, out var tableName) && _dataSources.TryGetValue(instanceName, out instance)) { var entity = instance.Entities.SingleOrDefault(e => e.LogicalName == tableName && @@ -569,7 +570,7 @@ public IEnumerable GetSuggestions(string text, int pos) } } - items.AddRange(attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).GroupBy(x => x.LogicalName).Where(g => g.Count() == 1).SelectMany(g => AttributeAutocompleteItem.CreateList(g.Single(), currentLength, false))); + items.AddRange(attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).GroupBy(x => x.LogicalName).Where(g => g.Count() == 1).SelectMany(g => AttributeAutocompleteItem.CreateList(g.Single(), currentLength, false, instance.Metadata))); items.AddRange(typeof(SqlFunctions).GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public).Select(m => new FunctionAutocompleteItem(m, currentLength))); @@ -579,7 +580,6 @@ public IEnumerable GetSuggestions(string text, int pos) { // Check if there are any applicable filter operator functions that match the type of the current attribute var identifiers = prevPrevWord.Split('.'); - var instance = default(AutocompleteDataSource); var entity = default(EntityMetadata); var attribute = default(AttributeMetadata); @@ -1694,15 +1694,21 @@ public AttributeAutocompleteItem(AttributeMetadata attribute, int replaceLength, _virtualSuffix = virtualSuffix; } - public static IEnumerable CreateList(AttributeMetadata attribute, int replaceLength, bool writeable) + public static IEnumerable CreateList(AttributeMetadata attribute, int replaceLength, bool writeable, IAttributeMetadataCache metadata) { yield return new AttributeAutocompleteItem(attribute, replaceLength); if (!writeable && (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata || attribute is LookupAttributeMetadata)) yield return new AttributeAutocompleteItem(attribute, replaceLength, "name"); - if (attribute is LookupAttributeMetadata lookup && lookup.Targets?.Length != 1 && lookup.AttributeType != AttributeTypeCode.PartyList && (lookup.EntityLogicalName != "listmember" || lookup.LogicalName != "entityid")) - yield return new AttributeAutocompleteItem(attribute, replaceLength, "type"); + if (attribute is LookupAttributeMetadata lookup) + { + if (lookup.Targets?.Length != 1 && lookup.AttributeType != AttributeTypeCode.PartyList && (lookup.EntityLogicalName != "listmember" || lookup.LogicalName != "entityid")) + yield return new AttributeAutocompleteItem(attribute, replaceLength, "type"); + + if (lookup.Targets != null && metadata.TryGetMinimalData(lookup.EntityLogicalName, out var entity) && entity.Attributes.Any(a => a.LogicalName == attribute.LogicalName + "pid")) + yield return new AttributeAutocompleteItem(attribute, replaceLength, "pid"); + } } public override string ToolTipTitle @@ -1721,6 +1727,8 @@ public override string ToolTipText description += $"\r\n\r\nThis attribute holds the display name of the {_attribute.LogicalName} field"; else if (_virtualSuffix == "type") description += $"\r\n\r\nThis attribute holds the logical name of the type of record referenced by the {_attribute.LogicalName} field"; + else if (_virtualSuffix == "pid") + description += $"\r\n\r\nThis attribute holds the partition id of the record referenced by the {_attribute.LogicalName} field"; else if (_attribute.AttributeType == AttributeTypeCode.Picklist) description += "\r\n\r\nThis attribute holds the underlying integer value of the field"; else if (_attribute.AttributeType == AttributeTypeCode.Lookup || _attribute.AttributeType == AttributeTypeCode.Customer || _attribute.AttributeType == AttributeTypeCode.Owner) diff --git a/MarkMpn.Sql4Cds.XTB/Autocomplete.cs b/MarkMpn.Sql4Cds.XTB/Autocomplete.cs index 3daaac2f..0bea33c1 100644 --- a/MarkMpn.Sql4Cds.XTB/Autocomplete.cs +++ b/MarkMpn.Sql4Cds.XTB/Autocomplete.cs @@ -432,7 +432,7 @@ public IEnumerable GetSuggestions(string text, int pos) if (tables.TryGetValue(targetTable, out var tableName)) { if (TryParseTableName(tableName, out var instanceName, out _, out tableName) && _dataSources.TryGetValue(instanceName, out var instance) && instance.Metadata.TryGetMinimalData(tableName, out var metadata)) - return FilterList(metadata.Attributes.Where(a => a.IsValidForUpdate != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(a => a.IsValidForUpdate != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true, instance.Metadata)).OrderBy(a => a), currentWord); } } @@ -455,14 +455,14 @@ public IEnumerable GetSuggestions(string text, int pos) if (instance.Messages.TryGetValue(messageName, out var message) && message.IsValidAsTableValuedFunction()) { - return FilterList(GetMessageOutputAttributes(message, instance).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false)).OrderBy(a => a), currentWord); + return FilterList(GetMessageOutputAttributes(message, instance).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false, instance.Metadata)).OrderBy(a => a), currentWord); } } else { // Table if (instance.Metadata.TryGetMinimalData((schemaName == "metadata" ? "metadata." : "") + tableName, out var metadata)) - return FilterList(metadata.Attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, false, instance.Metadata)).OrderBy(a => a), currentWord); } } } @@ -496,7 +496,7 @@ public IEnumerable GetSuggestions(string text, int pos) { attributeFilter = a => a.IsValidForCreate != false && a.AttributeOf == null; } - return FilterList(metadata.Attributes.Where(attributeFilter).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true)).OrderBy(a => a), currentWord); + return FilterList(metadata.Attributes.Where(attributeFilter).SelectMany(a => AttributeAutocompleteItem.CreateList(a, currentLength, true, instance.Metadata)).OrderBy(a => a), currentWord); } } else @@ -508,6 +508,7 @@ public IEnumerable GetSuggestions(string text, int pos) // * variables var items = new List(); var attributes = new List(); + var instance = default(AutocompleteDataSource); foreach (var table in tables) { @@ -517,7 +518,7 @@ public IEnumerable GetSuggestions(string text, int pos) var messageName = table.Value.Substring(0, table.Value.Length - 1); if (TryParseTableName(messageName, out var instanceName, out var schemaName, out var tableName) && - _dataSources.TryGetValue(instanceName, out var instance) && + _dataSources.TryGetValue(instanceName, out instance) && (String.IsNullOrEmpty(schemaName) || schemaName == "dbo") && instance.Messages.TryGetValue(messageName, out var message) && message.IsValidAsTableValuedFunction()) @@ -531,7 +532,7 @@ public IEnumerable GetSuggestions(string text, int pos) else { // Table - if (TryParseTableName(table.Value, out var instanceName, out var schemaName, out var tableName) && _dataSources.TryGetValue(instanceName, out var instance)) + if (TryParseTableName(table.Value, out var instanceName, out var schemaName, out var tableName) && _dataSources.TryGetValue(instanceName, out instance)) { var entity = instance.Entities.SingleOrDefault(e => e.LogicalName == tableName && @@ -568,7 +569,7 @@ public IEnumerable GetSuggestions(string text, int pos) } } - items.AddRange(attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).GroupBy(x => x.LogicalName).Where(g => g.Count() == 1).SelectMany(g => AttributeAutocompleteItem.CreateList(g.Single(), currentLength, false))); + items.AddRange(attributes.Where(a => a.IsValidForRead != false && a.AttributeOf == null).GroupBy(x => x.LogicalName).Where(g => g.Count() == 1).SelectMany(g => AttributeAutocompleteItem.CreateList(g.Single(), currentLength, false, instance.Metadata))); items.AddRange(typeof(FunctionMetadata.SqlFunctions).GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public).Select(m => new FunctionAutocompleteItem(m, currentLength))); @@ -578,7 +579,6 @@ public IEnumerable GetSuggestions(string text, int pos) { // Check if there are any applicable filter operator functions that match the type of the current attribute var identifiers = prevPrevWord.Split('.'); - var instance = default(AutocompleteDataSource); var entity = default(EntityMetadata); var attribute = default(AttributeMetadata); @@ -1676,15 +1676,21 @@ public AttributeAutocompleteItem(AttributeMetadata attribute, int replaceLength, _virtualSuffix = virtualSuffix; } - public static IEnumerable CreateList(AttributeMetadata attribute, int replaceLength, bool writeable) + public static IEnumerable CreateList(AttributeMetadata attribute, int replaceLength, bool writeable, IAttributeMetadataCache metadata) { yield return new AttributeAutocompleteItem(attribute, replaceLength); if (!writeable && (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata || attribute is LookupAttributeMetadata)) yield return new AttributeAutocompleteItem(attribute, replaceLength, "name"); - if (attribute is LookupAttributeMetadata lookup && lookup.Targets?.Length != 1 && lookup.AttributeType != AttributeTypeCode.PartyList && (lookup.EntityLogicalName != "listmember" || lookup.LogicalName != "entityid")) - yield return new AttributeAutocompleteItem(attribute, replaceLength, "type"); + if (attribute is LookupAttributeMetadata lookup) + { + if (lookup.Targets?.Length != 1 && lookup.AttributeType != AttributeTypeCode.PartyList && (lookup.EntityLogicalName != "listmember" || lookup.LogicalName != "entityid")) + yield return new AttributeAutocompleteItem(attribute, replaceLength, "type"); + + if (lookup.Targets != null && metadata.TryGetMinimalData(lookup.EntityLogicalName, out var entity) && entity.Attributes.Any(a => a.LogicalName == attribute.LogicalName + "pid")) + yield return new AttributeAutocompleteItem(attribute, replaceLength, "pid"); + } } public override string ToolTipTitle @@ -1703,6 +1709,8 @@ public override string ToolTipText description += $"\r\n\r\nThis attribute holds the display name of the {_attribute.LogicalName} field"; else if (_virtualSuffix == "type") description += $"\r\n\r\nThis attribute holds the logical name of the type of record referenced by the {_attribute.LogicalName} field"; + else if (_virtualSuffix == "pid") + description += $"\r\n\r\nThis attribute holds the partition id of the record referenced by the {_attribute.LogicalName} field"; else if (_attribute.AttributeType == AttributeTypeCode.Picklist) description += "\r\n\r\nThis attribute holds the underlying integer value of the field"; else if (_attribute.AttributeType == AttributeTypeCode.Lookup || _attribute.AttributeType == AttributeTypeCode.Customer || _attribute.AttributeType == AttributeTypeCode.Owner)