Skip to content

Commit

Permalink
Handle virtual entity providers returning values of different types t…
Browse files Browse the repository at this point in the history
…han described by the metadata

Fixes #152
  • Loading branch information
MarkMpn committed Jul 8, 2024
1 parent 6bcea62 commit cc9bc71
Showing 1 changed file with 70 additions and 3 deletions.
73 changes: 70 additions & 3 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public InvalidPagingException(string message) : base(message)
private List<KeyValuePair<string, string>> _pagingFields;
private List<INullable> _lastPageValues;
private bool _missingPagingCookie;
private bool _isVirtualEntity;

public FetchXmlScan()
{
Expand Down Expand Up @@ -291,6 +292,7 @@ protected override IEnumerable<Entity> ExecuteInternal(NodeExecutionContext cont
var mainEntity = FetchXml.Items.OfType<FetchEntityType>().Single();
var name = mainEntity.name;
var meta = dataSource.Metadata[name];
_isVirtualEntity = meta.DataProviderId != null && meta.DataProviderId != DataProviders.ElasticDataProvider;

if (!(Parent is PartitionedAggregateNode))
context.Options.Progress(0, $"Retrieving {GetDisplayName(0, meta)}...");
Expand Down Expand Up @@ -748,10 +750,69 @@ private void OnRetrievedEntity(Entity entity, INodeSchema schema, IQueryExecutio
{
object sqlValue;

if (entity.Attributes.TryGetValue(col.Key, out var value) && value != null)
sqlValue = SqlTypeConverter.NetToSqlType(dataSource, value, col.Value.Type);
else
if (!entity.Attributes.TryGetValue(col.Key, out var value) && _isVirtualEntity)
{
// Virtual entity providers aren't reliable and can produce attributes with names in different cases
// than expected, e.g. msdyn_solutioncomponentsummary returns its id as msdyn_solutioncomponentsummaryId
var altKey = entity.Attributes.Keys.FirstOrDefault(k => k.Equals(col.Key, StringComparison.OrdinalIgnoreCase));

if (altKey != null)
{
value = entity[col.Key] = entity[altKey];
entity.Attributes.Remove(altKey);
}
}

if (value == null)
{
sqlValue = SqlTypeConverter.GetNullValue(col.Value.Type.ToNetType(out _));
}
else
{
if (_isVirtualEntity)
{
// Virtual entity providers aren't reliable and can produce attribute values of different types
// than expected, e.g. msdyn_componentlayer returns string values as guids. Convert the CLR
// values to the correct type before converting to SQL types.
var expectedClrType = SqlTypeConverter.SqlToNetType(col.Value.Type.ToNetType(out _));
if (value.GetType() != expectedClrType)
{
if (value is Guid guidValue)
{
if (expectedClrType == typeof(string))
{
value = guidValue.ToString();
}
else if (expectedClrType == typeof(EntityReference))
{
// We don't know the logical name of the entity reference, check if we can find it from the metadata
var parts = col.Key.SplitMultiPartIdentifier();
var entityLogicalName = Entity.name;
if (!parts[0].Equals(Alias, StringComparison.OrdinalIgnoreCase))
entityLogicalName = Entity.FindLinkEntity(parts[0]).name;
var attrMeta = dataSource.Metadata[entityLogicalName].Attributes.Single(a => a.LogicalName == parts[1]);
if (attrMeta.IsPrimaryId == false && attrMeta is LookupAttributeMetadata lookupAttrMeta)
{
if (lookupAttrMeta.Targets?.Length == 1)
value = new EntityReference(lookupAttrMeta.Targets[0], guidValue);
else
value = new EntityReference(null, guidValue);
}
}
else
{
throw new QueryExecutionException($"Expected {expectedClrType.Name} value, got {value.GetType()}");
}
}
else
{
value = Convert.ChangeType(value, expectedClrType);
}
}
}

sqlValue = SqlTypeConverter.NetToSqlType(dataSource, value, col.Value.Type);
}

if (_primaryKeyColumns.TryGetValue(col.Key, out var logicalName) && sqlValue is SqlGuid guid)
sqlValue = new SqlEntityReference(DataSource, logicalName, guid);
Expand Down Expand Up @@ -955,6 +1016,7 @@ public override INodeSchema GetSchema(NodeCompilationContext context)
// Add each attribute from the main entity and recurse into link entities
var entity = FetchXml.Items.OfType<FetchEntityType>().Single();
var meta = dataSource.Metadata[entity.name];
_isVirtualEntity = meta.DataProviderId != null && meta.DataProviderId != DataProviders.ElasticDataProvider;

var schema = new ColumnList();
var aliases = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -1333,6 +1395,11 @@ private void AddSchemaAttribute(ColumnList schema, Dictionary<string, IReadOnlyL
if (parts.Length == 2 && HiddenAliases.Contains(parts[0]))
visible = false;

// Virtual entity providers are not reliable - they can produce string values that are longer than the metadata indicates
// e.g. msdn_componentlayer.msdyn_children
if (_isVirtualEntity && type is SqlDataTypeReferenceWithCollation sqlType && sqlType.SqlDataTypeOption == SqlDataTypeOption.NVarChar)
type = DataTypeHelpers.NVarChar(Int32.MaxValue, sqlType.Collation, sqlType.CollationLabel);

schema[fullName] = new ColumnDefinition(type, !notNull, false, visible);

if (simpleName == null)
Expand Down

0 comments on commit cc9bc71

Please sign in to comment.