Skip to content

Commit

Permalink
Refactored IF/WHILE into single Conditional node
Browse files Browse the repository at this point in the history
Added support for EXISTS/IN subqueries in IF/WHILE
  • Loading branch information
MarkMpn committed Feb 5, 2022
1 parent 6842126 commit 29b05f1
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 111 deletions.
38 changes: 33 additions & 5 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3698,8 +3698,9 @@ INSERT INTO account (name) VALUES ('one')

Assert.AreEqual(1, plans.Length);

var cond = AssertNode<IfNode>(plans[0]);
var cond = AssertNode<ConditionalNode>(plans[0]);
Assert.AreEqual("@param1 = 1", cond.Condition.ToSql());
Assert.AreEqual(ConditionalNodeType.If, cond.Type);

Assert.AreEqual(2, cond.TrueStatements.Length);
AssertNode<InsertNode>(cond.TrueStatements[0]);
Expand Down Expand Up @@ -3730,12 +3731,39 @@ INSERT INTO account (name) VALUES (@param1)

Assert.AreEqual(1, plans.Length);

var cond = AssertNode<WhileNode>(plans[0]);
var cond = AssertNode<ConditionalNode>(plans[0]);
Assert.AreEqual("@param1 < 10", cond.Condition.ToSql());
Assert.AreEqual(ConditionalNodeType.While, cond.Type);

Assert.AreEqual(2, cond.Statements.Length);
AssertNode<InsertNode>(cond.Statements[0]);
AssertNode<AssignVariablesNode>(cond.Statements[1]);
Assert.AreEqual(2, cond.TrueStatements.Length);
AssertNode<InsertNode>(cond.TrueStatements[0]);
AssertNode<AssignVariablesNode>(cond.TrueStatements[1]);
}

[TestMethod]
public void IfNotExists()
{
var metadata = new AttributeMetadataCache(_service);
var planBuilder = new ExecutionPlanBuilder(metadata, new StubTableSizeCache(), this);

var query = @"
IF NOT EXISTS(SELECT * FROM account WHERE name = @param1)
BEGIN
INSERT INTO account (name) VALUES (@param1)
END";

var parameters = new Dictionary<string, DataTypeReference>
{
["@param1"] = typeof(SqlString).ToSqlType()
};
var plans = planBuilder.Build(query, parameters, out _);

Assert.AreEqual(1, plans.Length);

var cond = AssertNode<ConditionalNode>(plans[0]);

Assert.AreEqual(1, cond.TrueStatements.Length);
AssertNode<InsertNode>(cond.TrueStatements[0]);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.Xrm.Sdk;

namespace MarkMpn.Sql4Cds.Engine.ExecutionPlan
{
class IfNode : BaseNode, IControlOfFlowNode
class ConditionalNode : BaseNode, IControlOfFlowNode
{
private int _executionCount;
private readonly Timer _timer = new Timer();
private Func<Entity, IDictionary<string, object>, IQueryExecutionOptions, bool> _condition;

public override int ExecutionCount => _executionCount;

Expand All @@ -25,10 +28,19 @@ class IfNode : BaseNode, IControlOfFlowNode
[Browsable(false)]
public int Length { get; set; }

[Category("If")]
[Browsable(false)]
public ConditionalNodeType Type { get; set; }

[Category("Conditional")]
[Description("The condition that must be true for execution to continue")]
public BooleanExpression Condition { get; set; }

[Browsable(false)]
public IDataExecutionPlanNodeInternal Source { get; set; }

[Browsable(false)]
public string SourceColumn { get; set; }

[Browsable(false)]
public IRootExecutionPlanNodeInternal[] TrueStatements { get; set; }

Expand All @@ -37,6 +49,9 @@ class IfNode : BaseNode, IControlOfFlowNode

public override void AddRequiredColumns(IDictionary<string, DataSource> dataSources, IDictionary<string, DataTypeReference> parameterTypes, IList<string> requiredColumns)
{
if (Source != null)
Source.AddRequiredColumns(dataSources, parameterTypes, new List<string>(requiredColumns));

foreach (var node in TrueStatements)
node.AddRequiredColumns(dataSources, parameterTypes, new List<string>(requiredColumns));

Expand All @@ -49,17 +64,54 @@ public override void AddRequiredColumns(IDictionary<string, DataSource> dataSour

public IRootExecutionPlanNodeInternal[] Execute(IDictionary<string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary<string, DataTypeReference> parameterTypes, IDictionary<string, object> parameterValues, out bool rerun)
{
rerun = false;
var expr = Condition.Compile(null, parameterTypes);

if (expr(null, parameterValues, options))
return TrueStatements;
else
return FalseStatements;
using (_timer.Run())
{
try
{
_executionCount++;
rerun = false;

bool result;

if (_condition != null)
{
result = _condition(null, parameterValues, options);
}
else
{
var record = Source.Execute(dataSources, options, parameterTypes, parameterValues).First();
result = ((SqlInt32)record[SourceColumn]).Value == 1;
}

if (result)
{
if (Type == ConditionalNodeType.While)
rerun = true;

return TrueStatements;
}

return FalseStatements;
}
catch (QueryExecutionException ex)
{
if (ex.Node == null)
ex.Node = this;

throw;
}
catch (Exception ex)
{
throw new QueryExecutionException(ex.Message, ex) { Node = this };
}
}
}

public IRootExecutionPlanNodeInternal FoldQuery(IDictionary<string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary<string, DataTypeReference> parameterTypes, IList<OptimizerHint> hints)
{
if (Source != null)
Source = Source.FoldQuery(dataSources, options, parameterTypes, hints);

for (var i = 0; i < TrueStatements.Length; i++)
TrueStatements[i] = TrueStatements[i].FoldQuery(dataSources, options, parameterTypes, hints);

Expand All @@ -69,15 +121,30 @@ public IRootExecutionPlanNodeInternal FoldQuery(IDictionary<string, DataSource>
FalseStatements[i] = FalseStatements[i].FoldQuery(dataSources, options, parameterTypes, hints);
}

_condition = Condition?.Compile(null, parameterTypes);

return this;
}

public override IEnumerable<IExecutionPlanNode> GetSources()
{
if (FalseStatements == null)
return TrueStatements;
if (Source != null)
yield return Source;

foreach (var stmt in TrueStatements)
yield return stmt;

return TrueStatements.Concat(FalseStatements);
if (FalseStatements != null)
{
foreach (var stmt in FalseStatements)
yield return stmt;
}
}
}

enum ConditionalNodeType
{
If,
While
}
}
70 changes: 0 additions & 70 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/WhileNode.cs

This file was deleted.

79 changes: 57 additions & 22 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,47 +159,82 @@ private void ConvertStatement(TSqlStatement statement, ExecutionPlanOptimizer op
}
}

private IRootExecutionPlanNodeInternal ConvertWhileStatement(WhileStatement whileStmt, ExecutionPlanOptimizer optimizer)
private IRootExecutionPlanNodeInternal ConvertIfWhileStatement(ConditionalNodeType type, BooleanExpression predicate, TSqlStatement trueStatement, TSqlStatement falseStatement, ExecutionPlanOptimizer optimizer)
{
// Check the predicate for errors
whileStmt.Predicate.GetType(null, null, _parameterTypes);

// Convert the inner statements
var queries = new List<IRootExecutionPlanNodeInternal>();
ConvertStatement(whileStmt.Statement, optimizer, queries);
// Check if the predicate is a simple expression or requires a query
var subqueryVisitor = new ScalarSubqueryVisitor();
predicate.Accept(subqueryVisitor);
IDataExecutionPlanNodeInternal predicateSource = null;
string sourceCol = null;

return new WhileNode
if (subqueryVisitor.Subqueries.Count == 0)
{
Condition = whileStmt.Predicate,
Statements = queries.ToArray()
};
}
// Check the predicate for errors
predicate.GetType(null, null, _parameterTypes);
}
else
{
// Convert predicate to query - IF EXISTS(qry) => SELECT CASE WHEN EXISTS(qry) THEN 1 ELSE 0 END
var select = new QuerySpecification
{
SelectElements =
{
new SelectScalarExpression
{
Expression = new SearchedCaseExpression
{
WhenClauses =
{
new SearchedWhenClause
{
WhenExpression = predicate,
ThenExpression = new IntegerLiteral { Value = "1" }
}
},
ElseExpression = new IntegerLiteral { Value = "0" }
}
}
}
};

private IRootExecutionPlanNodeInternal ConvertIfStatement(IfStatement ifStmt, ExecutionPlanOptimizer optimizer)
{
// Check the predicate for errors
ifStmt.Predicate.GetType(null, null, _parameterTypes);
var selectQry = ConvertSelectQuerySpec(select, Array.Empty<OptimizerHint>(), null, null, _parameterTypes);
predicateSource = selectQry.Source;
sourceCol = selectQry.ColumnSet[0].SourceColumn;
}

// Convert the true & false branches
var trueQueries = new List<IRootExecutionPlanNodeInternal>();
ConvertStatement(ifStmt.ThenStatement, optimizer, trueQueries);
ConvertStatement(trueStatement, optimizer, trueQueries);

List<IRootExecutionPlanNodeInternal> falseQueries = null;

if (ifStmt.ElseStatement != null)
if (falseStatement != null)
{
falseQueries = new List<IRootExecutionPlanNodeInternal>();
ConvertStatement(ifStmt.ElseStatement, optimizer, falseQueries);
ConvertStatement(falseStatement, optimizer, falseQueries);
}

return new IfNode
return new ConditionalNode
{
Condition = ifStmt.Predicate,
Condition = subqueryVisitor.Subqueries.Count == 0 ? predicate : null,
Source = predicateSource,
SourceColumn = sourceCol,
TrueStatements = trueQueries.ToArray(),
FalseStatements = falseQueries?.ToArray()
FalseStatements = falseQueries?.ToArray(),
Type = type
};
}

private IRootExecutionPlanNodeInternal ConvertWhileStatement(WhileStatement whileStmt, ExecutionPlanOptimizer optimizer)
{
return ConvertIfWhileStatement(ConditionalNodeType.While, whileStmt.Predicate, whileStmt.Statement, null, optimizer);
}

private IRootExecutionPlanNodeInternal ConvertIfStatement(IfStatement ifStmt, ExecutionPlanOptimizer optimizer)
{
return ConvertIfWhileStatement(ConditionalNodeType.If, ifStmt.Predicate, ifStmt.ThenStatement, ifStmt.ElseStatement, optimizer);
}

private IRootExecutionPlanNodeInternal ConvertSetVariableStatement(SetVariableStatement set)
{
if (set.CursorDefinition != null)
Expand Down
3 changes: 1 addition & 2 deletions MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\BulkDeleteJobNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\ComputeScalarNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\ConcatenateNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\ConditionalNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\ConstantScanNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\DeclareVariablesNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\DeleteNode.cs" />
Expand All @@ -58,8 +59,6 @@
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IDmlQueryExecutionPlanNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IExecutionPlanNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IFetchXmlExecutionPlanNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\WhileNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IfNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IImpersonateRevertExecutionPlanNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\IndexSpoolNode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ExecutionPlan\InsertNode.cs" />
Expand Down
Loading

0 comments on commit 29b05f1

Please sign in to comment.