Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into large-inserts
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkMpn committed Nov 7, 2024
2 parents 3343c79 + d8105b1 commit d4360d2
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 36 deletions.
16 changes: 8 additions & 8 deletions MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void Joins()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name AS account_name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid", NormalizeWhitespace(converted));
}

[TestMethod]
Expand All @@ -93,7 +93,7 @@ public void JoinFilter()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid AND account.name = 'data8' WHERE contact.firstname = 'Mark'", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name AS account_name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid AND account.name = 'data8' WHERE contact.firstname = 'Mark'", NormalizeWhitespace(converted));
}

[TestMethod]
Expand All @@ -113,7 +113,7 @@ public void JoinAlias()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, a.name FROM contact INNER JOIN account AS a ON contact.parentcustomerid = a.accountid", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, a.name AS a_name FROM contact INNER JOIN account AS a ON contact.parentcustomerid = a.accountid", NormalizeWhitespace(converted));
}

[TestMethod]
Expand All @@ -136,7 +136,7 @@ public void JoinAliasFilter()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, a.name FROM contact INNER JOIN account AS a ON contact.parentcustomerid = a.accountid WHERE a.name = 'data8'", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, a.name AS a_name FROM contact INNER JOIN account AS a ON contact.parentcustomerid = a.accountid WHERE a.name = 'data8'", NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down Expand Up @@ -228,7 +228,7 @@ public void JoinOrder()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid ORDER BY account.name ASC, contact.firstname ASC", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name AS account_name FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid ORDER BY account.name ASC, contact.firstname ASC", NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down Expand Up @@ -527,7 +527,7 @@ public void ArchiveJoins()

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

Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name FROM archive.contact INNER JOIN archive.account ON contact.parentcustomerid = account.accountid", NormalizeWhitespace(converted));
Assert.AreEqual("SELECT contact.firstname, contact.lastname, account.name AS account_name FROM archive.contact INNER JOIN archive.account ON contact.parentcustomerid = account.accountid", NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down Expand Up @@ -725,7 +725,7 @@ public void MatchFirstRowUsingCrossApply()
var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
SELECT contact.fullname, account.accountid, account.name FROM contact CROSS APPLY ( SELECT TOP 1 account.accountid, account.name FROM account WHERE contact.contactid = account.primarycontactid ) AS account"), NormalizeWhitespace(converted));
SELECT contact.fullname, account.accountid AS account_accountid, account.name AS account_name FROM contact CROSS APPLY ( SELECT TOP 1 account.accountid, account.name FROM account WHERE contact.contactid = account.primarycontactid ) AS account"), NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down Expand Up @@ -777,7 +777,7 @@ public void ColumnComparisonCrossTable()
var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _);

Assert.AreEqual(NormalizeWhitespace(@"
SELECT contact.contactid, contact.fullname, acct.name FROM contact LEFT OUTER JOIN account AS acct ON contact.parentcustomerid = acct.accountid WHERE contact.fullname = acct.name"), NormalizeWhitespace(converted));
SELECT contact.contactid, contact.fullname, acct.name AS acct_name FROM contact LEFT OUTER JOIN account AS acct ON contact.parentcustomerid = acct.accountid WHERE contact.fullname = acct.name"), NormalizeWhitespace(converted));
}

[TestMethod]
Expand Down
44 changes: 21 additions & 23 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -975,40 +975,38 @@ private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationConte
if (attribute is DateTimeAttributeMetadata && literals != null &&
(op == @operator.eq || op == @operator.ne || op == @operator.neq || op == @operator.gt || op == @operator.ge || op == @operator.lt || op == @operator.le || op == @operator.@in || op == @operator.notin))
{
var ecc = new ExpressionCompilationContext(context, null, null);
var eec = new ExpressionExecutionContext(ecc);

for (var i = 0; i < literals.Length; i++)
{
if (!(literals[i] is Literal lit))
continue;

try
{
DateTime dt;
lit.GetType(ecc, out var sourceType);
var targetType = attribute.GetAttributeSqlType(dataSource, false);

if (lit is StringLiteral)
dt = SqlDateTime.Parse(lit.Value).Value;
else if (lit is IntegerLiteral || lit is NumericLiteral || lit is RealLiteral)
dt = new DateTime(1900, 1, 1).AddDays(Double.Parse(lit.Value, CultureInfo.InvariantCulture));
else
throw new NotSupportedQueryFragmentException(Sql4CdsError.DateTimeParseError(lit));
if (!SqlTypeConverter.CanChangeTypeImplicit(sourceType, targetType))
throw new NotSupportedQueryFragmentException(Sql4CdsError.DateTimeParseError(lit));

DateTimeOffset dto;
var sqlValue = (INullable)lit.Compile(ecc)(eec);
var conversion = SqlTypeConverter.GetConversion(sourceType, targetType);
var datetimeValue = (SqlDateTime)conversion(sqlValue, eec);
var dt = datetimeValue.Value;

if (context.Options.UseLocalTimeZone)
dto = new DateTimeOffset(dt, TimeZoneInfo.Local.GetUtcOffset(dt));
else
dto = new DateTimeOffset(dt, TimeSpan.Zero);
DateTimeOffset dto;

var formatted = dto.ToString("yyyy-MM-ddTHH':'mm':'ss.FFFzzz");
if (context.Options.UseLocalTimeZone)
dto = new DateTimeOffset(dt, TimeZoneInfo.Local.GetUtcOffset(dt));
else
dto = new DateTimeOffset(dt, TimeSpan.Zero);

if (literals.Length == 1)
value = formatted;
var formatted = dto.ToString("yyyy-MM-ddTHH':'mm':'ss.FFFzzz");

values[i].Value = formatted;
}
catch (FormatException)
{
throw new NotSupportedQueryFragmentException(Sql4CdsError.DateTimeParseError(lit));
}
if (literals.Length == 1)
value = formatted;

values[i].Value = formatted;
}
}

Expand Down
14 changes: 14 additions & 0 deletions MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,22 @@ private void VerifyFilterValueTypes(string entityName, object[] items, DataSourc
break;
}

// Apply the standard SQL string -> target type conversion to each value - it will throw an exception if any values are in the incorrect format.
var conversion = SqlTypeConverter.GetConversion(DataTypeHelpers.NVarChar(Int32.MaxValue, dataSource.DefaultCollation, CollationLabel.CoercibleDefault), attrType);

// Special case: we convert datetime values to a different format to be understood by Dataverse which is different
// from the SQL formats in BaseDataNode.TranslateFetchXMLCriteriaWithVirtualAttributes
if (attrType.IsSameAs(DataTypeHelpers.DateTime))
{
conversion = (value, ctx) =>
{
if (value is SqlString str && DateTimeOffset.TryParseExact(str.Value, "yyyy-MM-ddTHH:mm:ss.FFFzzz", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
return new SqlDateTimeOffset(dt);

return conversion(value, ctx);
};
}

if (condition.value != null)
conversion(dataSource.DefaultCollation.ToSqlString(condition.value), context);

Expand Down
12 changes: 7 additions & 5 deletions MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m

// SELECT (columns from first table)
var entity = fetch.Items.OfType<FetchEntityType>().SingleOrDefault();
AddSelectElements(query, entity.Items, entity?.name);
AddSelectElements(query, entity.Items, entity?.name, true);

if (query.SelectElements.Count == 0)
query.SelectElements.Add(new SelectStarExpression());
Expand Down Expand Up @@ -256,7 +256,7 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m
/// <param name="query">The SQL query to append to the SELECT clause of</param>
/// <param name="items">The FetchXML items to process</param>
/// <param name="prefix">The name or alias of the table being processed</param>
private static void AddSelectElements(QuerySpecification query, object[] items, string prefix)
private static void AddSelectElements(QuerySpecification query, object[] items, string prefix, bool isRoot)
{
if (items == null)
return;
Expand Down Expand Up @@ -347,6 +347,8 @@ private static void AddSelectElements(QuerySpecification query, object[] items,
// Apply alias
if (!String.IsNullOrEmpty(attr.alias) && (attr.aggregateSpecified || attr.alias != attr.name))
element.ColumnName = new IdentifierOrValueExpression { Identifier = new Identifier { Value = attr.alias } };
else if (!isRoot)
element.ColumnName = new IdentifierOrValueExpression { Identifier = new Identifier { Value = $"{prefix}_{attr.name}" } };

query.SelectElements.Add(element);

Expand Down Expand Up @@ -464,8 +466,8 @@ private static TableReference BuildJoins(IOrganizationService org, IAttributeMet
}
else if (link.linktype == "matchfirstrowusingcrossapply")
{
AddSelectElements(subquery, link.Items, link.alias ?? link.name);
AddSelectElements(query, link.Items, link.alias ?? link.name);
AddSelectElements(subquery, link.Items, link.alias ?? link.name, true);
AddSelectElements(query, link.Items, link.alias ?? link.name, false);
}

subquery.FromClause = new FromClause
Expand Down Expand Up @@ -624,7 +626,7 @@ private static TableReference BuildJoins(IOrganizationService org, IAttributeMet
};

// Update the SELECT clause
AddSelectElements(query, link.Items, link.alias ?? link.name);
AddSelectElements(query, link.Items, link.alias ?? link.name, false);

// Handle any filters within the <link-entity> as additional join criteria
var filter = GetFilter(org, metadata, link.Items, link.alias ?? link.name, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday);
Expand Down

0 comments on commit d4360d2

Please sign in to comment.