Skip to content

Commit

Permalink
Support <fetch useraworderby>
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Mar 26, 2024
1 parent 2978607 commit 24f6125
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 23 deletions.
22 changes: 10 additions & 12 deletions MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,39 +115,37 @@ public void Order()
}

[TestMethod]
public void OrderLookup()
public void OrderPicklist()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch>
<entity name='contact'>
<attribute name='firstname' />
<attribute name='lastname' />
<order attribute='parentcustomerid' />
<entity name='new_customentity'>
<attribute name='new_name' />
<order attribute='new_optionsetvalue' />
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions(), out _);

Assert.AreEqual("SELECT firstname, lastname FROM contact ORDER BY parentcustomeridname ASC", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT new_name FROM new_customentity ORDER BY new_optionsetvaluename ASC", NormalizeWhitespace(converted));
}

[TestMethod]
public void OrderLookupRaw()
public void OrderPicklistRaw()
{
var metadata = new AttributeMetadataCache(_service);
var fetch = @"
<fetch useraworderby='1'>
<entity name='contact'>
<attribute name='firstname' />
<attribute name='lastname' />
<order attribute='parentcustomerid' />
<entity name='new_customentity'>
<attribute name='new_name' />
<order attribute='new_optionsetvalue' />
</entity>
</fetch>";

var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions(), out _);

Assert.AreEqual("SELECT firstname, lastname FROM contact ORDER BY parentcustomerid ASC", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT new_name FROM new_customentity ORDER BY new_optionsetvalue ASC", NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down
13 changes: 13 additions & 0 deletions MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/New_CustomEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,18 @@ class New_CustomEntity

[RelationshipSchemaName("new_customentity_children")]
public IEnumerable<New_CustomEntity> Children { get; }

[AttributeLogicalName("new_optionsetvalue")]
public New_OptionSet? New_OptionSetValue { get; set; }

[AttributeLogicalName("new_optionsetvaluename")]
public string New_OptionSetValueName { get; set; }
}

enum New_OptionSet
{
Value1 = 100001,
Value2,
Value3
}
}
71 changes: 71 additions & 0 deletions MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7187,5 +7187,76 @@ public void AliasSameAsVirtualAttribute()
</entity>
</fetch>");
}

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

var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvaluename";

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='new_customentity'>
<attribute name='new_customentityid' />
<order attribute='new_optionsetvalue' />
</entity>
</fetch>");
}

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

var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue";

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 useraworderby='1'>
<entity name='new_customentity'>
<attribute name='new_customentityid' />
<order attribute='new_optionsetvalue' />
</entity>
</fetch>");
}

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

var query = @"SELECT new_customentityid FROM new_customentity ORDER BY new_optionsetvalue, new_optionsetvaluename";

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

Assert.AreEqual(1, plans.Length);

var select = AssertNode<SelectNode>(plans[0]);
var sort = AssertNode<SortNode>(select.Source);
Assert.AreEqual(1, sort.PresortedCount);
Assert.AreEqual(2, sort.Sorts.Count);
Assert.AreEqual("new_customentity.new_optionsetvaluename", sort.Sorts[1].Expression.ToSql());
var fetch = AssertNode<FetchXmlScan>(sort.Source);
AssertFetchXml(fetch, @"
<fetch useraworderby='1'>
<entity name='new_customentity'>
<attribute name='new_customentityid' />
<attribute name='new_optionsetvalue' />
<order attribute='new_optionsetvalue' />
</entity>
</fetch>");
}
}
}
42 changes: 38 additions & 4 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context)
var fetchSchema = fetchXml.GetSchema(context);
var validOrder = true;
var currentEntity = 0;
bool? useRawOrderBy = null;

foreach (var sortOrder in Sorts)
{
Expand Down Expand Up @@ -375,17 +376,32 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context)
}
else
{
// Sorting on a lookup Guid column actually sorts by the associated name field, which isn't what we want
if (attribute is LookupAttributeMetadata || attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata)
// Sorting on a lookup Guid and picklist column actually sorts by the associated name field, which isn't what we want
// Picklist sorting can be controlled by the useraworderby flag though.
if (attribute is LookupAttributeMetadata)
return this;

if (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata)
{
if (useRawOrderBy == false)
return this;

useRawOrderBy = true;
}

// Sorts on the virtual ___name attribute should be applied to the underlying field
if (attribute == null && fetchSort.attribute.EndsWith("name") == true)
{
attribute = meta.Attributes.SingleOrDefault(a => a.LogicalName == fetchSort.attribute.Substring(0, fetchSort.attribute.Length - 4) && a.AttributeOf == null);

if (attribute != null)
{
if (attribute is LookupAttributeMetadata || useRawOrderBy == true)
return this;

fetchSort.attribute = attribute.LogicalName;
useRawOrderBy = false;
}
}
}

Expand All @@ -406,9 +422,18 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context)
var meta = dataSource.Metadata[linkEntity.name];
var attribute = meta.Attributes.SingleOrDefault(a => a.LogicalName == fetchSort.attribute && a.AttributeOf == null);

// Sorting on a lookup Guid column actually sorts by the associated name field, which isn't what we want
if (attribute is LookupAttributeMetadata || attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata)
// Sorting on a lookup Guid or picklist column actually sorts by the associated name field, which isn't what we want
// Picklist sorting can be controlled by the useraworderby flag though.
if (attribute is LookupAttributeMetadata)
return this;

if (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata)
{
if (useRawOrderBy == false)
return this;

useRawOrderBy = true;
}

// Sorting on multi-select picklist fields isn't supported in FetchXML
if (attribute is MultiSelectPicklistAttributeMetadata)
Expand All @@ -420,7 +445,13 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context)
attribute = meta.Attributes.SingleOrDefault(a => a.LogicalName == fetchSort.attribute.Substring(0, fetchSort.attribute.Length - 4) && a.AttributeOf == null);

if (attribute != null)
{
if (attribute is LookupAttributeMetadata || useRawOrderBy == true)
return this;

fetchSort.attribute = attribute.LogicalName;
useRawOrderBy = false;
}
}

if (attribute == null)
Expand Down Expand Up @@ -471,6 +502,9 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context)
}

PresortedCount++;

if (useRawOrderBy == true)
fetchXml.FetchXml.UseRawOrderBy = true;
}

return Source;
Expand Down
11 changes: 4 additions & 7 deletions MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2592,14 +2592,11 @@ private static void AddOrderBy(string name, object[] items, QuerySpecification q
if (!aliasToLogicalName.TryGetValue(entityAlias, out var entityLogicalName))
entityLogicalName = entityAlias;

if (!useRawOrderBy)
{
var entityMetadata = metadata[entityLogicalName];
var attr = entityMetadata.Attributes.SingleOrDefault(a => a.LogicalName == attributeName);
var entityMetadata = metadata[entityLogicalName];
var attr = entityMetadata.Attributes.SingleOrDefault(a => a.LogicalName == attributeName);

if (attr is LookupAttributeMetadata || attr is EnumAttributeMetadata)
attributeName += "name";
}
if (attr is LookupAttributeMetadata || ((attr is EnumAttributeMetadata || attr is BooleanAttributeMetadata) && !useRawOrderBy))
attributeName += "name";

query.OrderByClause.OrderByElements.Add(new ExpressionWithSortOrder
{
Expand Down

0 comments on commit 24f6125

Please sign in to comment.