Skip to content

Commit

Permalink
Added "any" and "not any" link entity types
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Mar 19, 2024
1 parent d9a4f49 commit 7473251
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 70 deletions.
142 changes: 135 additions & 7 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,6 +2210,134 @@ public void ExistsFilterCorrelated()
</fetch>");
}

[TestMethod]
public void ExistsFilterCorrelatedWithAny()
{
using (_localDataSource.EnableJoinOperator(JoinOperator.Any))
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

var query = @"
SELECT
accountid,
name
FROM
account
WHERE
EXISTS (SELECT * FROM contact WHERE parentcustomerid = accountid) OR
name = 'Data8'";

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

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var fetch = AssertNode<FetchXmlScan>(select.Source);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='accountid' />
<attribute name='name' />
<filter type='or'>
<link-entity name='contact' from='parentcustomerid' to='accountid' link-type='any'>
</link-entity>
<condition attribute='name' operator='eq' value='Data8' />
</filter>
<order attribute='accountid' />
</entity>
</fetch>");
}
}

[TestMethod]
public void ExistsFilterCorrelatedWithAnyParentAndChildAndAdditionalFilter()
{
using (_localDataSource.EnableJoinOperator(JoinOperator.Any))
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

var query = @"
SELECT
accountid,
name
FROM
account
WHERE
EXISTS (SELECT * FROM contact WHERE parentcustomerid = accountid AND firstname = 'Mark') AND
EXISTS (SELECT * FROM contact WHERE primarycontactid = contactid AND lastname = 'Carrington') AND
name = 'Data8'";

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

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var fetch = AssertNode<FetchXmlScan>(select.Source);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='accountid' />
<attribute name='name' />
<filter type='and'>
<link-entity name='contact' from='parentcustomerid' to='accountid' link-type='any'>
<filter>
<condition attribute='firstname' operator='eq' value='Mark' />
</filter>
</link-entity>
<link-entity name='contact' from='contactid' to='primarycontactid' link-type='any'>
<filter>
<condition attribute='lastname' operator='eq' value='Carrington' />
</filter>
</link-entity>
<condition attribute='name' operator='eq' value='Data8' />
</filter>
</entity>
</fetch>");
}
}

[TestMethod]
public void NotExistsFilterCorrelatedOnLinkEntity()
{
using (_localDataSource.EnableJoinOperator(JoinOperator.NotAny))
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

var query = @"
SELECT
accountid,
name
FROM
account
INNER JOIN contact ON account.primarycontactid = contact.contactid
WHERE
NOT EXISTS (SELECT * FROM account WHERE accountid = contact.parentcustomerid AND name = 'Data8')";

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

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var fetch = AssertNode<FetchXmlScan>(select.Source);
AssertFetchXml(fetch, @"
<fetch>
<entity name='account'>
<attribute name='accountid' />
<attribute name='name' />
<link-entity name='contact' alias='contact' from='contactid' to='primarycontactid' link-type='inner'>
<filter>
<link-entity name='account' from='accountid' to='parentcustomerid' link-type='not any'>
<filter>
<condition attribute='name' operator='eq' value='Data8' />
</filter>
</link-entity>
</filter>
</link-entity>
</entity>
</fetch>");
}
}

[TestMethod]
public void NotExistsFilterCorrelated()
{
Expand Down Expand Up @@ -3058,7 +3186,7 @@ public void FoldFilterWithInClauseOr()
[TestMethod]
public void FoldFilterWithInClauseWithoutPrimaryKey()
{
using (_localDataSource.EnableJoinOperator(JoinOperator.Any))
using (_localDataSource.EnableJoinOperator(JoinOperator.In))
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

Expand All @@ -3074,7 +3202,7 @@ public void FoldFilterWithInClauseWithoutPrimaryKey()
<fetch>
<entity name='account'>
<attribute name='name' />
<link-entity name='contact' alias='Expr1' from='createdon' to='createdon' link-type='in'>
<link-entity name='contact' from='createdon' to='createdon' link-type='in'>
<filter>
<condition attribute='firstname' operator='eq' value='Mark' />
</filter>
Expand Down Expand Up @@ -3121,7 +3249,7 @@ public void FoldNotInToLeftOuterJoin()
[TestMethod]
public void FoldFilterWithInClauseOnLinkEntityWithoutPrimaryKey()
{
using (_localDataSource.EnableJoinOperator(JoinOperator.Any))
using (_localDataSource.EnableJoinOperator(JoinOperator.In))
{
var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this);

Expand All @@ -3142,7 +3270,7 @@ public void FoldFilterWithInClauseOnLinkEntityWithoutPrimaryKey()
<condition attribute='name' operator='like' value='Data8%' />
</filter>
</link-entity>
<link-entity name='contact' alias='Expr1' from='createdon' to='createdon' link-type='in'>
<link-entity name='contact' from='createdon' to='createdon' link-type='in'>
<filter>
<condition attribute='firstname' operator='eq' value='Mark' />
</filter>
Expand Down Expand Up @@ -3171,7 +3299,7 @@ public void FoldFilterWithExistsClauseWithoutPrimaryKey()
<fetch>
<entity name='account'>
<attribute name='name' />
<link-entity name='contact' alias='contact' from='createdon' to='createdon' link-type='exists'>
<link-entity name='contact' from='createdon' to='createdon' link-type='exists'>
<filter>
<condition attribute='firstname' operator='eq' value='Mark' />
<condition attribute='createdon' operator='not-null' />
Expand Down Expand Up @@ -6738,7 +6866,7 @@ public void DoNotUseCustomPagingForInJoin()
{
// https://github.com/MarkMpn/Sql4Cds/issues/366

using (_dataSource.EnableJoinOperator(JoinOperator.Any))
using (_dataSource.EnableJoinOperator(JoinOperator.In))
{
var planBuilder = new ExecutionPlanBuilder(_dataSources.Values, new OptionsWrapper(this) { PrimaryDataSource = "uat" });

Expand All @@ -6755,7 +6883,7 @@ WHERE contactid IN (SELECT DISTINCT primarycontactid FROM account WHERE name =
<fetch xmlns:generator='MarkMpn.SQL4CDS'>
<entity name='contact'>
<attribute name='contactid' />
<link-entity name='account' from='primarycontactid' to='contactid' link-type='in' alias='Expr1'>
<link-entity name='account' from='primarycontactid' to='contactid' link-type='in'>
<filter>
<condition attribute='name' operator='eq' value='Data8' />
</filter>
Expand Down
23 changes: 17 additions & 6 deletions MarkMpn.Sql4Cds.Engine/DataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ public class DataSource
/// Creates a new <see cref="DataSource"/> using default values based on an existing connection.
/// </summary>
/// <param name="org">The <see cref="IOrganizationService"/> that provides the connection to the instance</param>
public DataSource(IOrganizationService org)
public DataSource(IOrganizationService org) : this(org, null, null, null)
{
Metadata = new AttributeMetadataCache(org);
TableSizeCache = new TableSizeCache(org, Metadata);
MessageCache = new MessageCache(org, Metadata);
}

public DataSource(IOrganizationService org, IAttributeMetadataCache metadata, ITableSizeCache tableSize, IMessageCache messages)
{
string name = null;
Version version = null;
Expand All @@ -42,7 +49,7 @@ public DataSource(IOrganizationService org)
version = svc.ConnectedOrgVersion;
}
#endif

if (name == null)
{
var orgDetails = org.RetrieveMultiple(new QueryExpression("organization") { ColumnSet = new ColumnSet("name") }).Entities[0];
Expand All @@ -56,10 +63,10 @@ public DataSource(IOrganizationService org)
}

Connection = org;
Metadata = new AttributeMetadataCache(org);
Metadata = metadata;
Name = name;
TableSizeCache = new TableSizeCache(org, Metadata);
MessageCache = new MessageCache(org, Metadata);
TableSizeCache = tableSize;
MessageCache = messages;

var joinOperators = new List<JoinOperator>
{
Expand All @@ -70,8 +77,12 @@ public DataSource(IOrganizationService org)
if (version >= new Version("9.1.0.17461"))
{
// First documented in SDK Version 9.0.2.25: Updated for 9.1.0.17461 CDS release
joinOperators.Add(JoinOperator.Any);
joinOperators.Add(JoinOperator.In);
joinOperators.Add(JoinOperator.Exists);
joinOperators.Add(JoinOperator.Any);
joinOperators.Add(JoinOperator.NotAny);
joinOperators.Add(JoinOperator.All);
joinOperators.Add(JoinOperator.NotAll);
}

JoinOperatorsAvailable = joinOperators;
Expand Down
51 changes: 45 additions & 6 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ namespace MarkMpn.Sql4Cds.Engine.ExecutionPlan
/// </summary>
abstract class BaseDataNode : BaseNode, IDataExecutionPlanNodeInternal
{
/// <summary>
/// Holds data about a subquery filter (IN, EXISTS) that is needed to process the multi-step conversion
/// </summary>
protected class ConvertedSubquery
{
/// <summary>
/// The join that is used to process the subquery
/// </summary>
public BaseJoinNode JoinNode { get; set; }

/// <summary>
/// The FetchXML equivalent of the subquery
/// </summary>
public FetchLinkEntityType Condition { get; set; }

/// <summary>
/// The link entity to add the <see cref="Condition"/> to
/// </summary>
public FetchLinkEntityType LinkEntity { get; set; }
}

private int _executionCount;
private readonly Timer _timer = new Timer();
private TimeSpan _additionalDuration;
Expand Down Expand Up @@ -204,9 +225,9 @@ public void MergeStatsFrom(BaseDataNode other)
/// <param name="items">The child items of the root entity in the FetchXML query</param>
/// <param name="filter">The FetchXML version of the <paramref name="criteria"/> that is generated by this method</param>
/// <returns><c>true</c> if the <paramref name="criteria"/> can be translated to FetchXML, or <c>false</c> otherwise</returns>
protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet<string> barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, out filter filter)
protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet<string> barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, Dictionary<BooleanExpression, ConvertedSubquery> subqueryExpressions, HashSet<BooleanExpression> replacedSubqueryExpression, out filter filter)
{
if (!TranslateFetchXMLCriteria(context, dataSource, criteria, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out var condition, out filter))
if (!TranslateFetchXMLCriteria(context, dataSource, criteria, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpression, out var condition, out filter))
return false;

if (condition != null)
Expand All @@ -230,16 +251,34 @@ protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSou
/// <param name="filter">The FetchXML version of the <paramref name="criteria"/> that is generated by this method when it covers multiple conditions</param>
/// <param name="condition">The FetchXML version of the <paramref name="criteria"/> that is generated by this method when it is for a single condition only</param>
/// <returns><c>true</c> if the <paramref name="criteria"/> can be translated to FetchXML, or <c>false</c> otherwise</returns>
private bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet<string> barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, out condition condition, out filter filter)
private bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet<string> barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, Dictionary<BooleanExpression, ConvertedSubquery> subqueryExpressions, HashSet<BooleanExpression> replacedSubqueryExpression, out condition condition, out filter filter)
{
condition = null;
filter = null;

if (criteria == null)
return false;

if (subqueryExpressions != null && subqueryExpressions.TryGetValue(criteria, out var subqueryExpression))
{
if (replacedSubqueryExpression != null)
replacedSubqueryExpression.Add(criteria);

filter = new filter
{
Items = new[]
{
(object) subqueryExpression.Condition
}
};
return true;
}

if (criteria is BooleanBinaryExpression binary)
{
if (!TranslateFetchXMLCriteria(context, dataSource, binary.FirstExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out var lhsCondition, out var lhsFilter))
if (!TranslateFetchXMLCriteria(context, dataSource, binary.FirstExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpression, out var lhsCondition, out var lhsFilter))
return false;
if (!TranslateFetchXMLCriteria(context, dataSource, binary.SecondExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out var rhsCondition, out var rhsFilter))
if (!TranslateFetchXMLCriteria(context, dataSource, binary.SecondExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpression, out var rhsCondition, out var rhsFilter))
return false;

filter = new filter
Expand All @@ -256,7 +295,7 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSourc

if (criteria is BooleanParenthesisExpression paren)
{
return TranslateFetchXMLCriteria(context, dataSource, paren.Expression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out condition, out filter);
return TranslateFetchXMLCriteria(context, dataSource, paren.Expression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpression, out condition, out filter);
}

if (criteria is DistinctPredicate distinct)
Expand Down
Loading

0 comments on commit 7473251

Please sign in to comment.