Skip to content

Commit

Permalink
Improved FetchXML to SQL conversion for hierarchical filters
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Oct 27, 2023
1 parent 9aaad16 commit 1a1c177
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 78 deletions.
34 changes: 34 additions & 0 deletions MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FakeXrmEasyTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using FakeXrmEasy.FakeMessageExecutors;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Metadata;

namespace MarkMpn.Sql4Cds.Engine.FetchXml.Tests
{
Expand All @@ -23,6 +24,39 @@ public FakeXrmEasyTestsBase()
_context.AddFakeMessageExecutor<WhoAmIRequest>(new WhoAmIHandler());

_service = _context.GetOrganizationService();

SetRelationships(_context);
}

private void SetRelationships(XrmFakedContext context)
{
foreach (var entity in context.CreateMetadataQuery())
{
if (entity.OneToManyRelationships == null)
typeof(EntityMetadata).GetProperty(nameof(EntityMetadata.OneToManyRelationships)).SetValue(entity, Array.Empty<OneToManyRelationshipMetadata>());

if (entity.ManyToOneRelationships == null)
typeof(EntityMetadata).GetProperty(nameof(EntityMetadata.ManyToOneRelationships)).SetValue(entity, Array.Empty<OneToManyRelationshipMetadata>());

if (entity.LogicalName == "account")
{
// Add parentaccountid relationship
var relationship = new OneToManyRelationshipMetadata
{
SchemaName = "account_parentaccount",
ReferencedEntity = "account",
ReferencedAttribute = "accountid",
ReferencingEntity = "account",
ReferencingAttribute = "parentaccountid",
IsHierarchical = true
};

typeof(EntityMetadata).GetProperty(nameof(EntityMetadata.OneToManyRelationships)).SetValue(entity, entity.OneToManyRelationships.Concat(new[] { relationship }).ToArray());
typeof(EntityMetadata).GetProperty(nameof(EntityMetadata.ManyToOneRelationships)).SetValue(entity, entity.ManyToOneRelationships.Concat(new[] { relationship }).ToArray());
}

context.SetEntityMetadata(entity);
}
}
}

Expand Down
122 changes: 121 additions & 1 deletion MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,129 @@ public void ArchiveJoins()
Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name FROM archive.contact INNER JOIN archive.account ON contact.parentcustomerid = account.accountid", NormalizeWhitespace(converted));
}

[TestMethod]
public void Under()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='account'>
<filter>
<condition attribute='accountid' operator='under' value='E2218046-F778-42F6-A8A7-772D0653349B' />
</filter>
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
WITH account_hierarchical(accountid) AS (
SELECT accountid FROM account WHERE parentaccountid = 'e2218046-f778-42f6-a8a7-772d0653349b'
UNION ALL
SELECT account.accountid FROM account INNER JOIN account_hierarchical ON account.parentaccountid = account_hierarchical.accountid
)
SELECT * FROM account WHERE accountid IN ( SELECT accountid FROM account_hierarchical )"), NormalizeWhitespace(converted));
}

[TestMethod]
public void EqOrUnder()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='account'>
<filter>
<condition attribute='accountid' operator='eq-or-under' value='E2218046-F778-42F6-A8A7-772D0653349B' />
</filter>
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
WITH account_hierarchical(accountid) AS (
SELECT accountid FROM account WHERE accountid = 'e2218046-f778-42f6-a8a7-772d0653349b'
UNION ALL
SELECT account.accountid FROM account INNER JOIN account_hierarchical ON account.parentaccountid = account_hierarchical.accountid
)
SELECT * FROM account WHERE accountid IN ( SELECT accountid FROM account_hierarchical )"), NormalizeWhitespace(converted));
}

[TestMethod]
public void NotUnder()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='account'>
<filter>
<condition attribute='accountid' operator='not-under' value='E2218046-F778-42F6-A8A7-772D0653349B' />
</filter>
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
WITH account_hierarchical(accountid) AS (
SELECT accountid FROM account WHERE accountid = 'e2218046-f778-42f6-a8a7-772d0653349b'
UNION ALL
SELECT account.accountid FROM account INNER JOIN account_hierarchical ON account.parentaccountid = account_hierarchical.accountid
)
SELECT * FROM account WHERE (accountid IS NULL OR accountid NOT IN ( SELECT accountid FROM account_hierarchical ))"), NormalizeWhitespace(converted));
}

[TestMethod]
public void Above()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='account'>
<filter>
<condition attribute='accountid' operator='above' value='E2218046-F778-42F6-A8A7-772D0653349B' />
</filter>
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
WITH account_hierarchical(accountid, parentaccountid) AS (
SELECT account.accountid, account.parentaccountid FROM account INNER JOIN account AS anchor ON account.accountid = anchor.parentaccountid WHERE anchor.accountid = 'e2218046-f778-42f6-a8a7-772d0653349b'
UNION ALL
SELECT account.accountid, account.parentaccountid FROM account INNER JOIN account_hierarchical ON account.accountid = account_hierarchical.parentaccountid
)
SELECT * FROM account WHERE accountid IN ( SELECT accountid FROM account_hierarchical )"), NormalizeWhitespace(converted));
}

[TestMethod]
public void EqOrAbove()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='account'>
<filter>
<condition attribute='accountid' operator='eq-or-above' value='E2218046-F778-42F6-A8A7-772D0653349B' />
</filter>
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
WITH account_hierarchical(accountid, parentaccountid) AS (
SELECT accountid, parentaccountid FROM account WHERE accountid = 'e2218046-f778-42f6-a8a7-772d0653349b'
UNION ALL
SELECT account.accountid, account.parentaccountid FROM account INNER JOIN account_hierarchical ON account.accountid = account_hierarchical.parentaccountid
)
SELECT * FROM account WHERE accountid IN ( SELECT accountid FROM account_hierarchical )"), NormalizeWhitespace(converted));
}

private static string NormalizeWhitespace(string s)
{
return Regex.Replace(s, "\\s+", " ");
return Regex.Replace(s, "\\s+", " ").Trim();
}
}
}
7 changes: 7 additions & 0 deletions MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,12 @@ class Account
[AttributeLogicalName("primarycontactid")]
[RelationshipSchemaName("account_primarycontact")]
public Contact PrimaryContact { get; set; }

[AttributeLogicalName("parentaccountid")]
public EntityReference ParentAccountId { get; set; }

[AttributeLogicalName("parentaccountid")]
[RelationshipSchemaName("account_parentaccount")]
public Account ParentAccount { get; set; }
}
}
Loading

0 comments on commit 1a1c177

Please sign in to comment.