Skip to content

Commit

Permalink
Merge branch 'master' into issue-templates
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn authored Nov 24, 2024
2 parents 26c6cc9 + 21a791e commit d17c901
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 90 deletions.
19 changes: 19 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using System.Collections.Concurrent;
using Microsoft.Xrm.Sdk.Query;

#if NETCOREAPP
using Microsoft.PowerPlatform.Dataverse.Client;
#else
Expand Down Expand Up @@ -117,6 +119,7 @@ class ParallelThreadState
private int[] _threadCountHistory;
private int[] _rpmHistory;
private float[] _batchSizeHistory;
private ConcurrentDictionary<Guid, string> _solutionNames;

/// <summary>
/// The SQL string that the query was converted from
Expand Down Expand Up @@ -1038,6 +1041,22 @@ protected virtual ExecuteMultipleResponse ExecuteMultiple(DataSource dataSource,
return (ExecuteMultipleResponse)dataSource.Execute(org, req);
}

protected string GetSolutionName(Guid solutionId, DataSource dataSource)
{
if (_solutionNames == null)
_solutionNames = new ConcurrentDictionary<Guid, string>();

return _solutionNames.GetOrAdd(solutionId, id =>
{
var solution = (RetrieveResponse)dataSource.Execute(dataSource.Connection, new RetrieveRequest
{
Target = new EntityReference("solution", id),
ColumnSet = new ColumnSet("uniquename")
});
return solution.Entity.GetAttributeValue<string>("uniquename");
});
}

public abstract object Clone();
}
}
18 changes: 13 additions & 5 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect
entity =>
{
eec.Entity = entity;
return CreateDeleteRequest(meta, eec, PrimaryIdAccessors.ToDictionary(a => a.TargetAttribute, a => a.Accessor));
return CreateDeleteRequest(meta, eec, PrimaryIdAccessors.ToDictionary(a => a.TargetAttribute, a => a.Accessor), dataSource);
},
new OperationNames
{
Expand Down Expand Up @@ -159,8 +159,9 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect
}
}

private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, ExpressionExecutionContext context, Dictionary<string, Func<ExpressionExecutionContext, object>> attributeAccessors)
private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, ExpressionExecutionContext context, Dictionary<string, Func<ExpressionExecutionContext, object>> attributeAccessors, DataSource dataSource)
{
// Special case messages for intersect entities
if (meta.LogicalName == "principalobjectaccess")
{
var objectId = (EntityReference)attributeAccessors["objectid"](context);
Expand All @@ -172,16 +173,23 @@ private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, ExpressionE
Revokee = principalId
};
}

// Special case messages for intersect entities
if (meta.LogicalName == "listmember")
else if (meta.LogicalName == "listmember")
{
return new RemoveMemberListRequest
{
ListId = (Guid)attributeAccessors["listid"](context),
EntityId = (Guid)attributeAccessors["entityid"](context)
};
}
else if (meta.LogicalName == "solutioncomponent")
{
return new RemoveSolutionComponentRequest
{
ComponentId = (Guid)attributeAccessors["objectid"](context),
ComponentType = ((OptionSetValue)attributeAccessors["componenttype"](context)).Value,
SolutionUniqueName = GetSolutionName(((EntityReference)attributeAccessors["solutionid"](context)).Id, dataSource)
};
}
else if (meta.IsIntersect == true)
{
var relationship = meta.ManyToManyRelationships.Single();
Expand Down
127 changes: 51 additions & 76 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/EntityReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ public static string[] GetPrimaryKeyFields(EntityMetadata metadata, out bool isI
return new[] { "objectid", "objecttypecode", "principalid", "principaltypecode" };
}

if (metadata.LogicalName == "solutioncomponent")
{
isIntersect = true;
return new[] { "objectid", "componenttype", "solutionid" };
}

isIntersect = false;

if (metadata.DataProviderId == DataProviders.ElasticDataProvider)
Expand Down Expand Up @@ -227,31 +233,26 @@ public List<AttributeAccessor> ValidateInsertColumnMapping(IList<ColumnReference

var accessors = ValidateInsertUpdateColumnMapping(DmlOperationDetails.Insert, colMappings, out var attributeNames);

// Special case: inserting into listmember requires listid and entityid
if (_metadata.LogicalName == "listmember")
{
if (!attributeNames.Contains("listid"))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = "listid" }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the listid column to be set" };
if (!attributeNames.Contains("entityid"))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = "entityid" }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the entityid column to be set" };
}
else if (_metadata.IsIntersect == true)
{
var relationship = _metadata.ManyToManyRelationships.Single();
if (!attributeNames.Contains(relationship.Entity1IntersectAttribute))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = relationship.Entity1IntersectAttribute }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the {relationship.Entity1IntersectAttribute} column to be set" };
if (!attributeNames.Contains(relationship.Entity2IntersectAttribute))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = relationship.Entity2IntersectAttribute }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the {relationship.Entity2IntersectAttribute} column to be set" };
}
else if (_metadata.LogicalName == "principalobjectaccess")
{
if (!attributeNames.Contains("objectid"))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = "objectid" }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the objectid column to be set" };
if (!attributeNames.Contains("principalid"))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = "principalid" }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the principalid column to be set" };
if (!attributeNames.Contains("accessrightsmask"))
throw new NotSupportedQueryFragmentException(Sql4CdsError.NotNullInsert(new Identifier { Value = "accessrightsmask" }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target)) { Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the accessrightsmask column to be set" };
}
var requiredAttributes = Array.Empty<string>();

// Special case: inserting into intersect tables requires the primary key columns to be set
var primaryKeyFields = GetPrimaryKeyFields(out var isIntersect);

if (isIntersect)
requiredAttributes = primaryKeyFields;

// Specialer case: lookup fields on principalobjectaccess could be set to EntityReference values,
// so typecode fields do not necessarily need to be set.
if (_metadata.LogicalName == "principalobjectaccess")
requiredAttributes = new[] { "objectid", "principalid", "accessrightsmask" };

var missingRequiredAttributeErrors = requiredAttributes
.Where(attr => !attributeNames.Contains(attr))
.Select(attr => new { Error = Sql4CdsError.NotNullInsert(new Identifier { Value = attr }, new Identifier { Value = _metadata.LogicalName }, "Insert", _target), Suggestion = $"Inserting values into the {_metadata.LogicalName} table requires the {attr} column to be set" })
.ToArray();

if (missingRequiredAttributeErrors.Any())
throw new NotSupportedQueryFragmentException(missingRequiredAttributeErrors.Select(e => e.Error).ToArray(), null) { Suggestion = String.Join(Environment.NewLine, missingRequiredAttributeErrors.Select(e => e.Suggestion)) };

return accessors;
}
Expand Down Expand Up @@ -287,44 +288,22 @@ public List<AttributeAccessor> ValidateUpdateNewValueColumnMapping(IDictionary<C
}
}

if (_metadata.IsIntersect == true)
var primaryKeyFields = GetPrimaryKeyFields(out var isIntersect);
if (isIntersect)
{
var manyToManyRelationship = _metadata.ManyToManyRelationships.Single();
// Intersect tables can only have their primary key columns updated
foreach (var col in mappings.Keys)
{
if (col.MultiPartIdentifier.Identifiers.Last().Value.Equals(manyToManyRelationship.Entity1IntersectAttribute, StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals(manyToManyRelationship.Entity2IntersectAttribute, StringComparison.OrdinalIgnoreCase))
if (primaryKeyFields.Contains(col.MultiPartIdentifier.Identifiers.Last().Value, StringComparer.OrdinalIgnoreCase))
continue;

errors.Add(Sql4CdsError.ReadOnlyColumn(col));
suggestions.Add($"Only the {manyToManyRelationship.Entity1IntersectAttribute} and {manyToManyRelationship.Entity2IntersectAttribute} columns can be used when updating values in the {_metadata.LogicalName} table");
}
}
else if (_metadata.LogicalName == "listmember")
{
foreach (var col in mappings.Keys)
{
if (col.MultiPartIdentifier.Identifiers.Last().Value.Equals("listid", StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals("entityid", StringComparison.OrdinalIgnoreCase))
// Special case: solutioncomponent can have its rootcomponentbehavior column updated
if (_metadata.LogicalName == "solutioncomponent" && col.MultiPartIdentifier.Identifiers.Last().Value.Equals("rootcomponentbehavior", StringComparison.OrdinalIgnoreCase))
continue;

errors.Add(Sql4CdsError.ReadOnlyColumn(col));
suggestions.Add("Only the listid and entityid columns can be used when updating values in the listmember table");
}
}
else if (_metadata.LogicalName == "principalobjectaccess")
{
foreach (var col in mappings.Keys)
{
if (col.MultiPartIdentifier.Identifiers.Last().Value.Equals("objectid", StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals("objecttypecode", StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals("principalid", StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals("principaltypecode", StringComparison.OrdinalIgnoreCase) ||
col.MultiPartIdentifier.Identifiers.Last().Value.Equals("accessrightsmask", StringComparison.OrdinalIgnoreCase))
continue;

errors.Add(Sql4CdsError.ReadOnlyColumn(col));
suggestions.Add("Only the objectid, principalid and accessrightsmask columns can be used when updating values in the principalobjectaccess table");
var primaryKeyFieldNames = string.Join(", ", primaryKeyFields.Take(primaryKeyFields.Length - 1).Select(f => f)) + " and " + primaryKeyFields.Last();
suggestions.Add($"Only the {primaryKeyFieldNames} columns can be used when updating values in the {_metadata.LogicalName} table");
}
}

Expand Down Expand Up @@ -380,6 +359,12 @@ private List<AttributeAccessor> ValidateInsertUpdateColumnMapping(DmlOperationDe
var attributes = _metadata.Attributes.ToDictionary(attr => attr.LogicalName, StringComparer.OrdinalIgnoreCase);
attributeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var primaryKeyFields = GetPrimaryKeyFields(out var isIntersect);

// Special case: solutioncomponent can have its rootcomponentbehavior column inserted/updated
if (_metadata.LogicalName == "solutioncomponent" && (operation == DmlOperationDetails.Insert || operation == DmlOperationDetails.UpdateNewValues || operation == DmlOperationDetails.UpdateExistingValues))
primaryKeyFields = primaryKeyFields.Concat(new[] { "rootcomponentbehavior" }).ToArray();

var contextParam = Expression.Parameter(typeof(ExpressionExecutionContext));
var entityParam = Expression.Property(contextParam, nameof(ExpressionExecutionContext.Entity));
var accessors = new List<AttributeAccessor>();
Expand Down Expand Up @@ -442,27 +427,7 @@ private List<AttributeAccessor> ValidateInsertUpdateColumnMapping(DmlOperationDe
continue;
}

if (_metadata.LogicalName == "listmember")
{
if (attr.LogicalName != "listid" && attr.LogicalName != "entityid")
{
errors.Add(Sql4CdsError.ReadOnlyColumn(col));
suggestions.Add($"Only the listid and entityid columns can be used when {operation.InProgressLowercase} values into the listmember table");
continue;
}
}
else if (_metadata.IsIntersect == true)
{
var relationship = _metadata.ManyToManyRelationships.Single();

if (attr.LogicalName != relationship.Entity1IntersectAttribute && attr.LogicalName != relationship.Entity2IntersectAttribute)
{
errors.Add(Sql4CdsError.ReadOnlyColumn(col));
suggestions.Add($"Only the {relationship.Entity1IntersectAttribute} and {relationship.Entity2IntersectAttribute} columns can be used when {operation.InProgressLowercase} values into the {_metadata.LogicalName} table");
continue;
}
}
else if (_metadata.LogicalName == "principalobjectaccess")
if (_metadata.LogicalName == "principalobjectaccess")
{
if (attr.LogicalName == "objecttypecode" || attr.LogicalName == "principaltypecode")
{
Expand All @@ -489,6 +454,16 @@ private List<AttributeAccessor> ValidateInsertUpdateColumnMapping(DmlOperationDe
continue;
}
}
else if (isIntersect)
{
if (!primaryKeyFields.Contains(attr.LogicalName))
{
errors.Add(Sql4CdsError.ReadOnlyColumn(col));
var primaryKeyFieldNames = string.Join(", ", primaryKeyFields.Take(primaryKeyFields.Length - 1).Select(f => f)) + " and " + primaryKeyFields.Last();
suggestions.Add($"Only the {primaryKeyFieldNames} columns can be used when {operation.InProgressLowercase} values into the {_metadata.LogicalName} table");
continue;
}
}
else
{
if (!operation.ValidAttributeFilter(attr))
Expand Down
24 changes: 22 additions & 2 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/InsertNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect
entity =>
{
eec.Entity = entity;
return CreateInsertRequest(meta, eec, attributeAccessors, primaryIdAccessor, attributes);
return CreateInsertRequest(meta, eec, attributeAccessors, primaryIdAccessor, attributes, dataSource);
},
new OperationNames
{
Expand Down Expand Up @@ -175,7 +175,7 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect
}
}

private OrganizationRequest CreateInsertRequest(EntityMetadata meta, ExpressionExecutionContext context, Dictionary<string,Func<ExpressionExecutionContext,object>> attributeAccessors, Func<ExpressionExecutionContext,object> primaryIdAccessor, Dictionary<string,AttributeMetadata> attributes)
private OrganizationRequest CreateInsertRequest(EntityMetadata meta, ExpressionExecutionContext context, Dictionary<string,Func<ExpressionExecutionContext,object>> attributeAccessors, Func<ExpressionExecutionContext,object> primaryIdAccessor, Dictionary<string,AttributeMetadata> attributes, DataSource dataSource)
{
// Special cases for intersect entities
if (LogicalName == "listmember")
Expand Down Expand Up @@ -225,6 +225,26 @@ private OrganizationRequest CreateInsertRequest(EntityMetadata meta, ExpressionE
};
}

if (LogicalName == "solutioncomponent")
{
var componentId = GetNotNull<Guid>("objectid", context, attributeAccessors);
var componentType = GetNotNull<OptionSetValue>("componenttype", context, attributeAccessors);
var solutionId = GetNotNull<EntityReference>("solutionid", context, attributeAccessors);
OptionSetValue rootComponentBehavior = null;
if (attributeAccessors.TryGetValue("rootcomponentbehavior", out var accessor))
rootComponentBehavior = (OptionSetValue)accessor(context);

return new AddSolutionComponentRequest
{
ComponentId = componentId,
ComponentType = componentType.Value,
SolutionUniqueName = GetSolutionName(solutionId.Id, dataSource),
DoNotIncludeSubcomponents = rootComponentBehavior != null && rootComponentBehavior.Value != 0,
IncludedComponentSettingsValues = rootComponentBehavior != null && rootComponentBehavior.Value == 2 ? Array.Empty<string>() : null,
AddRequiredComponents = false
};
}

var insert = new Entity(LogicalName);

if (primaryIdAccessor != null)
Expand Down
Loading

0 comments on commit d17c901

Please sign in to comment.