From 24f612570807e18f8077250e556eb5f597d9a317 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Tue, 26 Mar 2024 08:45:39 +0000 Subject: [PATCH] Support --- .../FetchXml2SqlTests.cs | 22 +++--- .../Metadata/New_CustomEntity.cs | 13 ++++ .../ExecutionPlanTests.cs | 71 +++++++++++++++++++ .../ExecutionPlan/SortNode.cs | 42 +++++++++-- MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs | 11 ++- 5 files changed, 136 insertions(+), 23 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs index 8b3f2aca..46213370 100644 --- a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs +++ b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs @@ -115,39 +115,37 @@ public void Order() } [TestMethod] - public void OrderLookup() + public void OrderPicklist() { var metadata = new AttributeMetadataCache(_service); var 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 = @" - - - - + + + "; 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] diff --git a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/New_CustomEntity.cs b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/New_CustomEntity.cs index e2f09e2d..7c6c1a42 100644 --- a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/New_CustomEntity.cs +++ b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/Metadata/New_CustomEntity.cs @@ -26,5 +26,18 @@ class New_CustomEntity [RelationshipSchemaName("new_customentity_children")] public IEnumerable 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 } } diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs index cf7a8bbd..fa620b60 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs @@ -7187,5 +7187,76 @@ public void AliasSameAsVirtualAttribute() "); } + + [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(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(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(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(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(plans[0]); + var sort = AssertNode(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(sort.Source); + AssertFetchXml(fetch, @" + + + + + + + "); + } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs index 16b28e02..d006568a 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs @@ -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) { @@ -375,9 +376,18 @@ 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) @@ -385,7 +395,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; + } } } @@ -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) @@ -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) @@ -471,6 +502,9 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) } PresortedCount++; + + if (useRawOrderBy == true) + fetchXml.FetchXml.UseRawOrderBy = true; } return Source; diff --git a/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs b/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs index 82c14fe6..4f713e57 100644 --- a/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs +++ b/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs @@ -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 {