Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added INSERT/UPDATE/DELETE support for solutioncomponent #590

Merged
merged 1 commit into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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