Skip to content

Commit

Permalink
Delete records by ID without reading them first
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Sep 27, 2024
1 parent ddc9cf9 commit e9d5d20
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 36 deletions.
15 changes: 15 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8534,5 +8534,20 @@ public void ScalarSubqueryWithoutAlias()
</entity>
</fetch>");
}

[TestMethod]
public void DeleteByIdUsesConstantScan()
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

var query = "DELETE FROM account WHERE accountid = '1D3AACA6-DEA4-490F-973E-E4181D4BE11C'";

var plans = planBuilder.Build(query, null, out _);

Assert.AreEqual(1, plans.Length);

var delete = AssertNode<DeleteNode>(plans[0]);
var constant = AssertNode<ConstantScanNode>(delete.Source);
}
}
}
36 changes: 2 additions & 34 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,40 +387,8 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSourc
}

// Select the correct FetchXML operator
@operator op;

switch (type)
{
case BooleanComparisonType.Equals:
case BooleanComparisonType.IsNotDistinctFrom:
op = @operator.eq;
break;

case BooleanComparisonType.GreaterThan:
op = @operator.gt;
break;

case BooleanComparisonType.GreaterThanOrEqualTo:
op = @operator.ge;
break;

case BooleanComparisonType.LessThan:
op = @operator.lt;
break;

case BooleanComparisonType.LessThanOrEqualTo:
op = @operator.le;
break;

case BooleanComparisonType.NotEqualToBrackets:
case BooleanComparisonType.NotEqualToExclamation:
case BooleanComparisonType.IsDistinctFrom:
op = @operator.ne;
break;

default:
throw new NotSupportedQueryFragmentException(Sql4CdsError.SyntaxError(comparison)) { Suggestion = "Unsupported comparison type" };
}
if (!type.TryConvertToFetchXml(out var op))
throw new NotSupportedQueryFragmentException(Sql4CdsError.SyntaxError(comparison)) { Suggestion = "Unsupported comparison type" };

// Find the entity that the condition applies to, which may be different to the entity that the condition FetchXML element will be
// added within
Expand Down
11 changes: 11 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDmlNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ private bool GetContinueOnError(NodeCompilationContext context, IList<OptimizerH
return continueOnError;
}

protected void FoldIdsToConstantScan(NodeCompilationContext context, IList<OptimizerHint> hints, string logicalName, Dictionary<string, string> columnMappings)
{
if (hints != null && hints.OfType<UseHintList>().Any(hint => hint.Hints.Any(s => s.Value.Equals("NO_DIRECT_DML", StringComparison.OrdinalIgnoreCase))))
return;

if (Source is FetchXmlScan fetch)
Source = fetch.FoldDmlSource(context, hints, logicalName, columnMappings);
else if (Source is SqlNode sql)
Source = sql.FoldDmlSource(context, hints, logicalName, columnMappings);
}

/// <summary>
/// Changes the name of source columns
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/DeleteNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ Source is FetchXmlScan fetch &&
}
}

// Replace a source query with a list of known IDs if possible
FoldIdsToConstantScan(context, hints, LogicalName, ColumnMappings);

return new[] { this };
}

Expand Down
44 changes: 44 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,50 @@ public static BooleanComparisonType TransitiveComparison(this BooleanComparisonT
}
}

/// <summary>
/// Returns the equivalent Fetch XML condition operator for this comparison
/// </summary>
/// <param name="cmp">The comparison type to convert to Fetch XML</param>
/// <returns>The equivalent Fetch XML condition operator for this comparison</returns>
public static bool TryConvertToFetchXml(this BooleanComparisonType cmp, out @operator op)
{
switch (cmp)
{
case BooleanComparisonType.Equals:
case BooleanComparisonType.IsNotDistinctFrom:
op = @operator.eq;
break;

case BooleanComparisonType.GreaterThan:
op = @operator.gt;
break;

case BooleanComparisonType.GreaterThanOrEqualTo:
op = @operator.ge;
break;

case BooleanComparisonType.LessThan:
op = @operator.lt;
break;

case BooleanComparisonType.LessThanOrEqualTo:
op = @operator.le;
break;

case BooleanComparisonType.NotEqualToBrackets:
case BooleanComparisonType.NotEqualToExclamation:
case BooleanComparisonType.IsDistinctFrom:
op = @operator.ne;
break;

default:
op = @operator.eq;
return false;
}

return true;
}

private static string GetTypeKey(DataTypeReference type, bool includeStringLength)
{
if (type is XmlDataTypeReference)
Expand Down
77 changes: 77 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2549,6 +2549,83 @@ protected override IEnumerable<string> GetVariablesInternal()
return FindParameterizedConditions().Keys;
}

internal IExecutionPlanNodeInternal FoldDmlSource(NodeCompilationContext context, IList<OptimizerHint> hints, string logicalName, Dictionary<string, string> mappings)
{
if (Entity.name != logicalName)
return this;

if (Entity.GetLinkEntities().Any())
return this;

var filters = Entity.Items.OfType<filter>().ToList();

if (filters.Count != 1)
return this;

if (!filters[0].Items.All(x => x is condition))
return this;

var dataSource = context.DataSources[DataSource];
var metadata = dataSource.Metadata[logicalName];
var conditions = filters[0].Items.Cast<condition>().ToList();
var schema = GetSchema(context);
var constantScan = new ConstantScanNode
{
Alias = Alias
};

foreach (var col in mappings)
constantScan.Schema[col.Value.SplitMultiPartIdentifier().Last()] = schema.Schema[col.Value];

// We can handle compound keys, but only if they are all ANDed together
if (mappings.Count > 1 && filters[0].type == filterType.and)
{
var values = new Dictionary<string, ScalarExpression>();

foreach (var mapping in mappings)
{
var condition = conditions.FirstOrDefault(c => c.attribute == mapping.Value.SplitMultiPartIdentifier()[1]);
if (condition == null)
return this;

if (condition.@operator != @operator.eq)
return this;

var attribute = metadata.Attributes.Single(a => a.LogicalName == condition.attribute);
values[condition.attribute] = attribute.GetDmlValue(condition.value, dataSource);
}

constantScan.Values.Add(values);
return constantScan;
}

// We can also handle multiple values for a single key being ORed together
else if (mappings.Count == 1 &&
conditions.All(c => c.attribute == metadata.PrimaryIdAttribute) &&
conditions.All(c => c.@operator == @operator.eq || c.@operator == @operator.@in) &&
(conditions.Count == 1 || filters[0].type == filterType.or))
{
foreach (var condition in conditions)
{
var attribute = metadata.Attributes.Single(a => a.LogicalName == condition.attribute);

if (condition.@operator == @operator.eq)
{
constantScan.Values.Add(new Dictionary<string, ScalarExpression> { [condition.attribute] = attribute.GetDmlValue(condition.value, dataSource) });
}
else if (condition.@operator == @operator.@in)
{
foreach (var value in condition.Items)
constantScan.Values.Add(new Dictionary<string, ScalarExpression> { [condition.attribute] = attribute.GetDmlValue(condition.value, dataSource) });
}
}

return constantScan;
}

return this;
}

public override string ToString()
{
return "FetchXML Query";
Expand Down
116 changes: 115 additions & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlan/SqlNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MarkMpn.Sql4Cds.Engine.FetchXml;
using MarkMpn.Sql4Cds.Engine.Visitors;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Metadata;

#if NETCOREAPP
using Microsoft.PowerPlatform.Dataverse.Client;
#else
Expand Down Expand Up @@ -53,6 +57,8 @@ public SqlNode() { }
[Browsable(false)]
public HashSet<string> Parameters { get; private set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

internal SelectStatement SelectStatement { get; set; }

public override void AddRequiredColumns(NodeCompilationContext context, IList<string> requiredColumns)
{
}
Expand Down Expand Up @@ -225,6 +231,113 @@ public override IEnumerable<IExecutionPlanNode> GetSources()
return Array.Empty<IExecutionPlanNode>();
}

internal IExecutionPlanNodeInternal FoldDmlSource(NodeCompilationContext context, IList<OptimizerHint> hints, string logicalName, Dictionary<string, string> mappings)
{
if (!(SelectStatement?.QueryExpression is QuerySpecification querySpec))
return this;

if (querySpec.FromClause == null || querySpec.FromClause.TableReferences.Count != 1 || !(querySpec.FromClause.TableReferences[0] is NamedTableReference table))
return this;

if (table.SchemaObject.BaseIdentifier.Value != logicalName)
return this;

if (querySpec.WhereClause == null || querySpec.WhereClause.SearchCondition == null)
return this;

var filterVisitor = new SimpleFilterVisitor();
querySpec.WhereClause.SearchCondition.Accept(filterVisitor);

if (filterVisitor.BinaryType == null)
return this;

var dataSource = context.DataSources[DataSource];
var metadata = dataSource.Metadata[logicalName];
var conditions = filterVisitor.Conditions.ToList();

if (!TryGetDmlSchema(dataSource, metadata, querySpec, out var schema))
return this;

var constantScan = new ConstantScanNode();

foreach (var col in mappings)
constantScan.Schema[col.Value.SplitMultiPartIdentifier().Last()] = new ColumnDefinition(schema[col.Value], true, false);

// We can handle compound keys, but only if they are all ANDed together
if (mappings.Count > 1 && filterVisitor.BinaryType == BooleanBinaryExpressionType.And)
{
var values = new Dictionary<string, ScalarExpression>();

foreach (var mapping in mappings)
{
var condition = conditions.FirstOrDefault(c => c.attribute == mapping.Value.SplitMultiPartIdentifier()[1]);
if (condition == null)
return this;

if (condition.@operator != @operator.eq)
return this;

var attribute = metadata.Attributes.Single(a => a.LogicalName == condition.attribute);
values[condition.attribute] = attribute.GetDmlValue(condition.value, dataSource);
}

constantScan.Values.Add(values);
return constantScan;
}

// We can also handle multiple values for a single key being ORed together
else if (mappings.Count == 1 &&
conditions.All(c => c.attribute == metadata.PrimaryIdAttribute) &&
conditions.All(c => c.@operator == @operator.eq || c.@operator == @operator.@in) &&
(conditions.Count == 1 || filterVisitor.BinaryType == BooleanBinaryExpressionType.Or))
{
foreach (var condition in conditions)
{
var attribute = metadata.Attributes.Single(a => a.LogicalName == condition.attribute);

if (condition.@operator == @operator.eq)
{
constantScan.Values.Add(new Dictionary<string, ScalarExpression> { [condition.attribute] = attribute.GetDmlValue(condition.value, dataSource) });
}
else if (condition.@operator == @operator.@in)
{
foreach (var value in condition.Items)
constantScan.Values.Add(new Dictionary<string, ScalarExpression> { [condition.attribute] = attribute.GetDmlValue(condition.value, dataSource) });
}
}

return constantScan;
}

return this;
}

private bool TryGetDmlSchema(DataSource dataSource, EntityMetadata metadata, QuerySpecification querySpec, out Dictionary<string, DataTypeReference> schema)
{
schema = new Dictionary<string, DataTypeReference>(StringComparer.OrdinalIgnoreCase);

foreach (var select in querySpec.SelectElements)
{
if (!(select is SelectScalarExpression scalar))
return false;

if (!(scalar.Expression is ColumnReferenceExpression col))
return false;

var attribute = metadata.Attributes.SingleOrDefault(a => a.LogicalName.Equals(col.MultiPartIdentifier.Identifiers.Last().Value, StringComparison.OrdinalIgnoreCase));

if (attribute == null)
return false;

var type = attribute.GetAttributeSqlType(dataSource, false);
var name = scalar.ColumnName?.Value ?? attribute.LogicalName;

schema[name] = type;
}

return true;
}

public override string ToString()
{
return "TDS Endpoint";
Expand All @@ -239,7 +352,8 @@ public object Clone()
Index = Index,
Length = Length,
LineNumber = LineNumber,
Parameters = Parameters
Parameters = Parameters,
SelectStatement = SelectStatement
};
}
}
Expand Down
3 changes: 2 additions & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2627,7 +2627,8 @@ private IRootExecutionPlanNodeInternal ConvertSelectStatement(SelectStatement se
var sql = new SqlNode
{
DataSource = Options.PrimaryDataSource,
Sql = select.ToSql()
Sql = select.ToSql(),
SelectStatement = select
};

var variables = new VariableCollectingVisitor();
Expand Down
Loading

0 comments on commit e9d5d20

Please sign in to comment.