From 98d757e5e9d64aebfbbb89dc93136b69664b2b1f Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Sat, 21 Sep 2024 18:53:47 +0100 Subject: [PATCH] Support UPDATE & DELETE of activitypointer records, and principalobjectaccess records that reference an activitypointer Fixes #540 --- .../ExecutionPlanTests.cs | 2 +- .../ExecutionPlan/BaseDmlNode.cs | 9 +- .../ExecutionPlan/DeleteNode.cs | 185 ++++------- .../ExecutionPlan/UpdateNode.cs | 42 ++- .../ExecutionPlanBuilder.cs | 309 +++++++++++------- .../ReplacePrimaryFunctionsVisitor.cs | 4 +- 6 files changed, 301 insertions(+), 250 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs index d7bd7638..33827bb5 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs @@ -3371,7 +3371,7 @@ public void SimpleDelete() var delete = AssertNode(plans[0]); Assert.AreEqual("account", delete.LogicalName); - Assert.AreEqual("account.accountid", delete.PrimaryIdSource); + Assert.AreEqual("account.accountid", delete.ColumnMappings["accountid"]); var fetch = AssertNode(delete.Source); AssertFetchXml(fetch, @" diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs index 56f08f65..e404fcc4 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs @@ -405,9 +405,6 @@ protected Dictionary> CompileColumnMappings(DataSou if (!attributes.TryGetValue(destAttributeName, out var attr) || attr.AttributeOf != null) continue; - if (metadata.LogicalName == "principalobjectaccess" && (attr.LogicalName == "objecttypecode" || attr.LogicalName == "principaltypecode")) - continue; - var sourceSqlType = schema.Schema[sourceColumnName].Type; var destType = attr.GetAttributeType(); var destSqlType = attr.IsPrimaryId == true ? DataTypeHelpers.UniqueIdentifier : attr.GetAttributeSqlType(dataSource, true); @@ -435,8 +432,7 @@ protected Dictionary> CompileColumnMappings(DataSou Expression convertedExpr; var lookupAttr = attr as LookupAttributeMetadata; - if (lookupAttr != null && lookupAttr.AttributeType != AttributeTypeCode.PartyList && metadata.IsIntersect != true || - metadata.LogicalName == "principalobjectaccess" && (attr.LogicalName == "objectid" || attr.LogicalName == "principalid")) + if (lookupAttr != null && lookupAttr.AttributeType != AttributeTypeCode.PartyList && metadata.IsIntersect != true) { if (sourceSqlType.IsSameAs(DataTypeHelpers.EntityReference)) { @@ -461,9 +457,6 @@ protected Dictionary> CompileColumnMappings(DataSou { var typeColName = destAttributeName + "type"; - if (metadata.LogicalName == "principalobjectaccess" && (attr.LogicalName == "objectid" || attr.LogicalName == "principalid")) - typeColName = destAttributeName.Replace("id", "typecode"); - var sourceTargetColumnName = mappings[typeColName]; var sourceTargetType = schema.Schema[sourceTargetColumnName].Type; diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs index 05afdf10..e7063661 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs @@ -35,28 +35,12 @@ class DeleteNode : BaseDmlNode public string LogicalName { get; set; } /// - /// The column that contains the primary ID of the records to delete + /// The columns that are used for the delete requests and their corresponding source columns /// [Category("Delete")] - [Description("The column that contains the primary ID of the records to delete")] - [DisplayName("PrimaryId Source")] - public string PrimaryIdSource { get; set; } - - /// - /// The column that contains the secondary ID of the records to delete (used for many-to-many intersect and elastic tables) - /// - [Category("Delete")] - [Description("The column that contains the secondary ID of the records to delete (used for many-to-many intersect and elastic tables)")] - [DisplayName("SecondaryId Source")] - public string SecondaryIdSource { get; set; } - - /// - /// The column that contains the type code of the records to delete (used for activity records) - /// - [Category("Delete")] - [Description("The column that contains the type code of the records to delete (used for activity records)")] - [DisplayName("ActivityTypeCode Source")] - public string ActivityTypeCodeSource { get; set; } + [Description("The columns that are used for the delete requests and their corresponding source columns")] + [DisplayName("Column Mappings")] + public Dictionary ColumnMappings { get; set; } [Category("Delete")] public override int MaxDOP { get; set; } @@ -74,14 +58,11 @@ class DeleteNode : BaseDmlNode public override void AddRequiredColumns(NodeCompilationContext context, IList requiredColumns) { - if (!requiredColumns.Contains(PrimaryIdSource)) - requiredColumns.Add(PrimaryIdSource); - - if (SecondaryIdSource != null && !requiredColumns.Contains(SecondaryIdSource)) - requiredColumns.Add(SecondaryIdSource); - - if (ActivityTypeCodeSource != null && !requiredColumns.Contains(ActivityTypeCodeSource)) - requiredColumns.Add(ActivityTypeCodeSource); + foreach (var col in ColumnMappings.Values) + { + if (!requiredColumns.Contains(col)) + requiredColumns.Add(col); + } Source.AddRequiredColumns(context, requiredColumns); } @@ -100,11 +81,15 @@ public override IRootExecutionPlanNodeInternal[] FoldQuery(NodeCompilationContex if ((context.Options.UseBulkDelete || LogicalName == "audit") && Source is FetchXmlScan fetch && LogicalName == fetch.Entity.name && - PrimaryIdSource.Equals($"{fetch.Alias}.{dataSource.Metadata[LogicalName].PrimaryIdAttribute}") && - String.IsNullOrEmpty(SecondaryIdSource) && - String.IsNullOrEmpty(ActivityTypeCodeSource)) + ColumnMappings.Count == 1) { - return new[] { new BulkDeleteJobNode { DataSource = DataSource, Source = fetch } }; + var metadata = dataSource.Metadata[LogicalName]; + + if (ColumnMappings.TryGetValue(metadata.PrimaryIdAttribute, out var primaryIdSource) && + primaryIdSource == $"{fetch.Alias}.{dataSource.Metadata[LogicalName].PrimaryIdAttribute}") + { + return new[] { new BulkDeleteJobNode { DataSource = DataSource, Source = fetch } }; + } } return new[] { this }; @@ -112,14 +97,15 @@ Source is FetchXmlScan fetch && protected override void RenameSourceColumns(IDictionary columnRenamings) { - if (columnRenamings.TryGetValue(PrimaryIdSource, out var primaryIdSourceRenamed)) - PrimaryIdSource = primaryIdSourceRenamed; + var renamedMappings = new Dictionary(); - if (SecondaryIdSource != null && columnRenamings.TryGetValue(SecondaryIdSource, out var secondaryIdSourceRenamed)) - SecondaryIdSource = secondaryIdSourceRenamed; - - if (ActivityTypeCodeSource != null && columnRenamings.TryGetValue(ActivityTypeCodeSource, out var activityTypeCodeSourceRenamed)) - ActivityTypeCodeSource = activityTypeCodeSourceRenamed; + foreach (var mapping in ColumnMappings) + { + if (columnRenamings.TryGetValue(mapping.Value, out var renamed)) + renamedMappings[mapping.Key] = renamed; + else + renamedMappings[mapping.Key] = mapping.Value; + } } public override void Execute(NodeExecutionContext context, out int recordsAffected, out string message) @@ -133,9 +119,8 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect List entities; EntityMetadata meta; - Func primaryIdAccessor; - Func secondaryIdAccessor = null; - + Dictionary> attributeAccessors; + using (_timer.Run()) { entities = GetDmlSourceEntities(context, out var schema); @@ -143,50 +128,24 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect // Precompile mappings with type conversions meta = dataSource.Metadata[LogicalName]; var dateTimeKind = context.Options.UseLocalTimeZone ? DateTimeKind.Local : DateTimeKind.Utc; - var primaryKey = meta.PrimaryIdAttribute; - string secondaryKey = null; - // Special cases for the keys used for intersect entities - if (meta.LogicalName == "listmember") - { - primaryKey = "listid"; - secondaryKey = "entityid"; - } - else if (meta.IsIntersect == true) - { - var relationship = meta.ManyToManyRelationships.Single(); - primaryKey = relationship.Entity1IntersectAttribute; - secondaryKey = relationship.Entity2IntersectAttribute; - } - else if (meta.LogicalName == "principalobjectaccess") - { - primaryKey = "objectid"; - secondaryKey = "principalid"; - } - else if (meta.DataProviderId == DataProviders.ElasticDataProvider) - { - secondaryKey = "partitionid"; - } + var mappingsToCompile = ColumnMappings; - var fullMappings = new Dictionary + // Entity type codes will be presented as a string but the mapping compilation will try to convert + // them to an int, so remove it and we can access the string directly + if (LogicalName == "principalobjectaccess") { - [primaryKey] = PrimaryIdSource - }; - - if (secondaryKey != null) - fullMappings[secondaryKey] = SecondaryIdSource; - - if (meta.LogicalName == "principalobjectaccess") + mappingsToCompile = new Dictionary(ColumnMappings); + mappingsToCompile.Remove("objecttypecode"); + mappingsToCompile.Remove("principaltypecode"); + } + else if (LogicalName == "activitypointer") { - fullMappings["objecttypecode"] = PrimaryIdSource.Replace("id", "typecode"); - fullMappings["principaltypecode"] = SecondaryIdSource.Replace("id", "typecode"); + mappingsToCompile = new Dictionary(ColumnMappings); + mappingsToCompile.Remove("activitytypecode"); } - var attributeAccessors = CompileColumnMappings(dataSource, LogicalName, fullMappings, schema, dateTimeKind, entities); - primaryIdAccessor = attributeAccessors[primaryKey]; - - if (SecondaryIdSource != null) - secondaryIdAccessor = attributeAccessors[secondaryKey]; + attributeAccessors = CompileColumnMappings(dataSource, LogicalName, mappingsToCompile, schema, dateTimeKind, entities); } // Check again that the update is allowed. Don't count any UI interaction in the execution time @@ -204,7 +163,7 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect context.Options, entities, meta, - entity => CreateDeleteRequest(meta, entity, primaryIdAccessor, secondaryIdAccessor), + entity => CreateDeleteRequest(meta, entity, attributeAccessors), new OperationNames { InProgressUppercase = "Deleting", @@ -229,50 +188,47 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect } } - private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, Entity entity, Func primaryIdAccessor, Func secondaryIdAccessor) + private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, Entity entity, Dictionary> attributeAccessors) { if (meta.LogicalName == "principalobjectaccess") { - var revoke = new RevokeAccessRequest + var objectId = (Guid)attributeAccessors["objectid"](entity); + var objectTypeCode = entity.GetAttributeValue(ColumnMappings["objecttypecode"]).Value; + var principalId = (Guid)attributeAccessors["principalid"](entity); + var principalTypeCode = entity.GetAttributeValue(ColumnMappings["principaltypecode"]).Value; + + return new RevokeAccessRequest { - Target = (EntityReference)primaryIdAccessor(entity), - Revokee = (EntityReference)secondaryIdAccessor(entity) + Target = new EntityReference(objectTypeCode, objectId), + Revokee = new EntityReference(principalTypeCode, principalId) }; - - // Special case for activitypointer - need to set the specific activity type code - var activityTypeCode = entity.GetAttributeValue(ActivityTypeCodeSource); - if (!activityTypeCode.IsNull) - revoke.Target.LogicalName = activityTypeCode.Value; - - return revoke; } - var id = (Guid)primaryIdAccessor(entity); - // Special case messages for intersect entities - if (meta.IsIntersect == true) + if (meta.LogicalName == "listmember") { - var secondaryId = (Guid)secondaryIdAccessor(entity); - - if (meta.LogicalName == "listmember") + return new RemoveMemberListRequest { - return new RemoveMemberListRequest - { - ListId = id, - EntityId = secondaryId - }; - } - + ListId = (Guid)attributeAccessors["listid"](entity), + EntityId = (Guid)attributeAccessors["entityid"](entity) + }; + } + else if (meta.IsIntersect == true) + { var relationship = meta.ManyToManyRelationships.Single(); + var targetId = (Guid)attributeAccessors[relationship.Entity1IntersectAttribute](entity); + var relatedId = (Guid)attributeAccessors[relationship.Entity2IntersectAttribute](entity); + return new DisassociateRequest { - Target = new EntityReference(relationship.Entity1LogicalName, id), - RelatedEntities = new EntityReferenceCollection { new EntityReference(relationship.Entity2LogicalName, secondaryId) }, + Target = new EntityReference(relationship.Entity1LogicalName, targetId), + RelatedEntities = new EntityReferenceCollection { new EntityReference(relationship.Entity2LogicalName, relatedId) }, Relationship = new Relationship(relationship.SchemaName) { PrimaryEntityRole = EntityRole.Referencing } }; } + var id = (Guid)attributeAccessors[meta.PrimaryIdAttribute](entity); var req = new DeleteRequest { Target = new EntityReference(LogicalName, id) @@ -286,18 +242,14 @@ private OrganizationRequest CreateDeleteRequest(EntityMetadata meta, Entity enti KeyAttributes = { [meta.PrimaryIdAttribute] = id, - ["partitionid"] = secondaryIdAccessor(entity) + ["partitionid"] = attributeAccessors["partitionid"](entity) } }; } // Special case for activitypointer - need to set the specific activity type code - if (ActivityTypeCodeSource != null) - { - var activityTypeCode = entity.GetAttributeValue(ActivityTypeCodeSource); - if (!activityTypeCode.IsNull) - req.Target.LogicalName = activityTypeCode.Value; - } + if (LogicalName == "activitypointer") + req.Target.LogicalName = entity.GetAttributeValue(ColumnMappings["activitytypecode"]).Value; return req; } @@ -317,6 +269,7 @@ protected override ExecuteMultipleResponse ExecuteMultiple(DataSource dataSource return base.ExecuteMultiple(dataSource, org, meta, req); if (meta.DataProviderId == DataProviders.ElasticDataProvider + && req.Requests.Cast().GroupBy(r => r.Target.LogicalName).Count() == 1 // DeleteMultiple is only supported on elastic tables, even if other tables do define the message /* || dataSource.MessageCache.IsMessageAvailable(meta.LogicalName, "DeleteMultiple")*/) { @@ -428,9 +381,7 @@ public override object Clone() Length = Length, LogicalName = LogicalName, MaxDOP = MaxDOP, - PrimaryIdSource = PrimaryIdSource, - SecondaryIdSource = SecondaryIdSource, - ActivityTypeCodeSource = ActivityTypeCodeSource, + ColumnMappings = ColumnMappings, Source = (IExecutionPlanNodeInternal)Source.Clone(), Sql = Sql }; diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/UpdateNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/UpdateNode.cs index 55cf3e06..3bf0dbe2 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/UpdateNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/UpdateNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Data.SqlTypes; using System.Linq; using System.ServiceModel; using System.Threading; @@ -139,6 +140,19 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect var dateTimeKind = context.Options.UseLocalTimeZone ? DateTimeKind.Local : DateTimeKind.Utc; var fullMappings = new Dictionary(ColumnMappings); fullMappings[meta.PrimaryIdAttribute] = new UpdateMapping { OldValueColumn = PrimaryIdSource, NewValueColumn = PrimaryIdSource }; + + // Entity type codes will be presented as a string but the mapping compilation will try to convert + // them to an int, so remove it and we can access the string directly + if (LogicalName == "principalobjectaccess") + { + fullMappings.Remove("objecttypecode"); + fullMappings.Remove("principaltypecode"); + } + else if (LogicalName == "activitypointer") + { + fullMappings.Remove("activitytypecode"); + } + newAttributeAccessors = CompileColumnMappings(dataSource, LogicalName, fullMappings.Where(kvp => kvp.Value.NewValueColumn != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.NewValueColumn), schema, dateTimeKind, entities); oldAttributeAccessors = CompileColumnMappings(dataSource, LogicalName, fullMappings.Where(kvp => kvp.Value.OldValueColumn != null).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.OldValueColumn), schema, dateTimeKind, entities); primaryIdAccessor = newAttributeAccessors[meta.PrimaryIdAttribute]; @@ -270,11 +284,15 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect } else if (meta.LogicalName == "principalobjectaccess") { - var objectIdPrev = preImage.GetAttributeValue("objectid"); - var principalIdPrev = preImage.GetAttributeValue("principalid"); + var objectIdPrev = preImage.GetAttributeValue("objectid"); + var objectTypeCodePrev = entity.GetAttributeValue(ColumnMappings["objecttypecode"].OldValueColumn).Value; + var principalIdPrev = preImage.GetAttributeValue("principalid"); + var principalTypeCodePrev = entity.GetAttributeValue(ColumnMappings["principaltypecode"].OldValueColumn).Value; var accessMaskPrev = (AccessRights)preImage.GetAttributeValue("accessrightsmask"); - var objectIdNew = update.GetAttributeValue("objectid") ?? objectIdPrev; - var principalIdNew = update.GetAttributeValue("principalid") ?? principalIdPrev; + var objectIdNew = update.GetAttributeValue("objectid") ?? objectIdPrev; + var objectTypeCodeNew = ColumnMappings["objecttypecode"].NewValueColumn != null ? update.GetAttributeValue(ColumnMappings["objecttypecode"].NewValueColumn).Value : objectTypeCodePrev; + var principalIdNew = update.GetAttributeValue("principalid") ?? principalIdPrev; + var principalTypeCodeNew = ColumnMappings["principaltypecode"].NewValueColumn != null ? update.GetAttributeValue(ColumnMappings["principaltypecode"].NewValueColumn).Value : principalTypeCodePrev; var accessMaskNew = (AccessRights?)update.GetAttributeValue("accessrightsmask") ?? accessMaskPrev; // Check if we need to remove any previous share permissions @@ -282,8 +300,8 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect { requests.Add(new RevokeAccessRequest { - Target = objectIdPrev, - Revokee = principalIdPrev + Target = new EntityReference(objectTypeCodePrev, objectIdPrev), + Revokee = new EntityReference(principalTypeCodePrev, principalIdPrev) }); } @@ -292,10 +310,10 @@ public override void Execute(NodeExecutionContext context, out int recordsAffect { requests.Add(new GrantAccessRequest { - Target = objectIdNew, + Target = new EntityReference(objectTypeCodeNew, objectIdNew), PrincipalAccess = new PrincipalAccess { - Principal = principalIdNew, + Principal = new EntityReference(principalTypeCodeNew, principalIdNew), AccessMask = accessMaskNew } }); @@ -569,6 +587,10 @@ private Entity ExtractEntity(Entity entity, EntityMetadata meta, Dictionary(ColumnMappings["activitytypecode"].OldValueColumn).Value; + return update; } @@ -601,7 +623,9 @@ protected override ExecuteMultipleResponse ExecuteMultiple(DataSource dataSource if (!req.Requests.All(r => r is UpdateRequest)) return base.ExecuteMultiple(dataSource, org, meta, req); - if (meta.DataProviderId == DataProviders.ElasticDataProvider || meta.DataProviderId == null && dataSource.MessageCache.IsMessageAvailable(meta.LogicalName, "UpdateMultiple")) + if (meta.DataProviderId == DataProviders.ElasticDataProvider || meta.DataProviderId == null && + dataSource.MessageCache.IsMessageAvailable(meta.LogicalName, "UpdateMultiple") && + req.Requests.Cast().GroupBy(r => r.Target.LogicalName).Count() == 1) { // Elastic tables can use UpdateMultiple for better performance than ExecuteMultiple var entities = new EntityCollection { EntityName = meta.LogicalName }; diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index ec4ad3e6..bcb9b974 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -1736,35 +1736,40 @@ private DeleteNode ConvertDeleteStatement(DeleteSpecification delete, IList(); + var columnMappings = new Dictionary(); if (targetMetadata.LogicalName == "listmember") { - primaryKey = "listid"; - secondaryKey = "entityid"; + columnMappings["listid"] = "listid"; + columnMappings["entityid"] = "entityid"; } else if (targetMetadata.IsIntersect == true) { var relationship = targetMetadata.ManyToManyRelationships.Single(); - primaryKey = relationship.Entity1IntersectAttribute; - secondaryKey = relationship.Entity2IntersectAttribute; + columnMappings[relationship.Entity1IntersectAttribute] = relationship.Entity1IntersectAttribute; + columnMappings[relationship.Entity2IntersectAttribute] = relationship.Entity2IntersectAttribute; } else if (targetMetadata.DataProviderId == DataProviders.ElasticDataProvider) { // Elastic tables need the partitionid as part of the primary key - secondaryKey = "partitionid"; + columnMappings[targetMetadata.PrimaryIdAttribute] = targetMetadata.PrimaryIdAttribute; + columnMappings["partitionid"] = "partitionid"; } else if (targetMetadata.LogicalName == "principalobjectaccess") { - primaryKey = "objectid"; - secondaryKey = "principalid"; + columnMappings["objectid"] = "objectid"; + columnMappings["objecttypecode"] = "objecttypecode"; + columnMappings["principalid"] = "principalid"; + columnMappings["principaltypecode"] = "principaltypecode"; } else if (targetMetadata.LogicalName == "activitypointer") { - activityTypeCode = "activitytypecode"; + columnMappings["activityid"] = "activityid"; + columnMappings["activitytypecode"] = "activitytypecode"; + } + else + { + columnMappings[targetMetadata.PrimaryIdAttribute] = targetMetadata.PrimaryIdAttribute; } if (deleteTarget.TargetSchema?.Equals("bin", StringComparison.OrdinalIgnoreCase) == true) @@ -1775,9 +1780,11 @@ private DeleteNode ConvertDeleteStatement(DeleteSpecification delete, IList 1) throw new NotSupportedQueryFragmentException(Sql4CdsError.NotSupported(delete, "Recycle bin records using a composite key cannot be deleted directly. Delete the associated record from the dbo.deleteitemreference table instead")); + var primaryKey = columnMappings.Single().Key; + queryExpression.SelectElements.Add(new SelectScalarExpression { Expression = new ColumnReferenceExpression @@ -1871,152 +1878,139 @@ private DeleteNode ConvertDeleteStatement(DeleteSpecification delete, IList c.OutputColumn == primaryKey).SourceColumn; + deleteNode.ColumnMappings = new Dictionary(); - if (secondaryKey != null) - deleteNode.SecondaryIdSource = select.ColumnSet.Single(c => c.OutputColumn == secondaryKey).SourceColumn; - - if (activityTypeCode != null) - deleteNode.ActivityTypeCodeSource = select.ColumnSet.Single(c => c.OutputColumn == activityTypeCode).SourceColumn; + foreach (var columnMapping in columnMappings) + deleteNode.ColumnMappings[columnMapping.Key] = select.ColumnSet.Single(c => c.OutputColumn == columnMapping.Key).SourceColumn; } else { deleteNode.Source = source; - deleteNode.PrimaryIdSource = primaryKey; - deleteNode.SecondaryIdSource = secondaryKey; - deleteNode.ActivityTypeCodeSource = activityTypeCode; + deleteNode.ColumnMappings = columnMappings; } return deleteNode; @@ -2295,21 +2289,109 @@ attr is LookupAttributeMetadata elasticLookupAttr && existingAttributes.Add("accessrightsmask"); } + // activitypointer is polymorphic so we need to include the activitytypecode + if (targetLogicalName == "activitypointer") + { + existingAttributes.Add("activitytypecode"); + } + foreach (var existingAttribute in existingAttributes) { - queryExpression.SelectElements.Add(new SelectScalarExpression + var expression = (ScalarExpression)new ColumnReferenceExpression { - Expression = new ColumnReferenceExpression + MultiPartIdentifier = new MultiPartIdentifier { - MultiPartIdentifier = new MultiPartIdentifier + Identifiers = { - Identifiers = + new Identifier { Value = targetAlias }, + new Identifier { Value = existingAttribute } + } + } + }; + + if (targetLogicalName == "principalobjectaccess" && existingAttribute == "objecttypecode") + { + // In case any of the records are for an activity, include the activitytypecode by joining to the activitypointer table + expression = new CoalesceExpression + { + Expressions = + { + new ScalarSubquery { - new Identifier { Value = targetAlias }, - new Identifier { Value = existingAttribute } - } + QueryExpression = new QuerySpecification + { + SelectElements = + { + new SelectScalarExpression + { + Expression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier { Value = "activitypointer" }, + new Identifier { Value = "activitytypecode" } + } + } + } + } + }, + FromClause = new FromClause + { + TableReferences = + { + new NamedTableReference + { + SchemaObject = new SchemaObjectName + { + Identifiers = + { + new Identifier { Value = "activitypointer" } + } + } + } + } + }, + WhereClause = new WhereClause + { + SearchCondition = new BooleanComparisonExpression + { + FirstExpression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier { Value = targetAlias }, + new Identifier { Value = "objectid" } + } + } + }, + ComparisonType = BooleanComparisonType.Equals, + SecondExpression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier { Value = "activitypointer" }, + new Identifier { Value = "activityid" } + } + } + } + } + } + } + }, + expression } - }, + }; + } + + queryExpression.SelectElements.Add(new SelectScalarExpression + { + Expression = expression, ColumnName = new IdentifierOrValueExpression { Identifier = new Identifier { Value = "existing_" + existingAttribute } @@ -2320,6 +2402,7 @@ attr is LookupAttributeMetadata elasticLookupAttr && var selectStatement = new SelectStatement { QueryExpression = queryExpression }; CopyDmlHintsToSelectStatement(hints, selectStatement); + selectStatement.Accept(new ReplacePrimaryFunctionsVisitor()); var source = ConvertSelectStatement(selectStatement); // Add UPDATE diff --git a/MarkMpn.Sql4Cds.Engine/Visitors/ReplacePrimaryFunctionsVisitor.cs b/MarkMpn.Sql4Cds.Engine/Visitors/ReplacePrimaryFunctionsVisitor.cs index dc6ea3db..e815d833 100644 --- a/MarkMpn.Sql4Cds.Engine/Visitors/ReplacePrimaryFunctionsVisitor.cs +++ b/MarkMpn.Sql4Cds.Engine/Visitors/ReplacePrimaryFunctionsVisitor.cs @@ -27,7 +27,7 @@ protected override ScalarExpression ReplaceExpression(ScalarExpression expressio foreach (var expr in coalesce.Expressions) caseExpr.WhenClauses.Add(new SearchedWhenClause { - WhenExpression = new BooleanIsNullExpression { Expression = expr, IsNot = true }, + WhenExpression = new BooleanIsNullExpression { Expression = expr.Clone(), IsNot = true }, ThenExpression = expr }); @@ -76,7 +76,7 @@ protected override ScalarExpression ReplaceExpression(ScalarExpression expressio WhenExpression = new BooleanComparisonExpression { ComparisonType = BooleanComparisonType.Equals, - FirstExpression = nullif.FirstExpression, + FirstExpression = nullif.FirstExpression.Clone(), SecondExpression = nullif.SecondExpression }, ThenExpression = new NullLiteral()