diff --git a/AzureDataStudioExtension/CHANGELOG.md b/AzureDataStudioExtension/CHANGELOG.md index ea1cfc9f..0500eebf 100644 --- a/AzureDataStudioExtension/CHANGELOG.md +++ b/AzureDataStudioExtension/CHANGELOG.md @@ -1,15 +1,26 @@ # Change Log -## [v8.0.1](https://github.com/MarkMpn/Sql4Cds/releases/tag/v8.0.1) - 2023-12-13 +## [v9.0.0](https://github.com/MarkMpn/Sql4Cds/releases/tag/v8.0.1) - 2024-04-12 +Added support for latest Fetch XML features +Support `TRY`, `CATCH` & `THROW` statements and related functions +Error handling consistency with SQL Server +Improved performance with large numbers of expressions and large `VALUES` lists Generate the execution plan for each statement in a batch only when necessary, to allow initial statements to succeed regardless of errors in later statements +Allow access to catalog views using TDS Endpoint +Inproved `EXECUTE AS` support +Handle missing values in XML `.value()` method +Detect TDS Endpoint incompatibility with XML data type methods and error handling functions +Fixed use of `TOP 1` with `IN` expression Fixed escaping column names for `SELECT` and `INSERT` commands Improved setting a partylist attribute based on an EntityReference value Fixed sorting results for `UNION` -Fold `DISTNCT` to data source for `UNION` +Fold `DISTINCT` to data source for `UNION` Fold groupings without aggregates to `DISTINCT` Fixed folding filters through nested loops with outer references Fixed use of recursive CTE references within subquery +Improved performance of `CROSS APPLY` and `OUTER APPLY` +Improved query cancellation ## [v8.0.0](https://github.com/MarkMpn/Sql4Cds/releases/tag/v8.0.0) - 2023-11-25 diff --git a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs index 21cdd4ae..46213370 100644 --- a/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs +++ b/MarkMpn.Sql4Cds.Engine.FetchXml.Tests/FetchXml2SqlTests.cs @@ -114,6 +114,40 @@ public void Order() Assert.AreEqual("SELECT firstname, lastname FROM contact ORDER BY firstname ASC", NormalizeWhitespace(converted)); } + [TestMethod] + public void OrderPicklist() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions(), out _); + + Assert.AreEqual("SELECT new_name FROM new_customentity ORDER BY new_optionsetvaluename ASC", NormalizeWhitespace(converted)); + } + + [TestMethod] + public void OrderPicklistRaw() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions(), out _); + + Assert.AreEqual("SELECT new_name FROM new_customentity ORDER BY new_optionsetvalue ASC", NormalizeWhitespace(converted)); + } + [TestMethod] public void OrderDescending() { @@ -132,6 +166,28 @@ public void OrderDescending() Assert.AreEqual("SELECT firstname, lastname FROM contact ORDER BY firstname DESC", NormalizeWhitespace(converted)); } + [TestMethod] + public void JoinOrder() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + "; + + 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)); + } + [TestMethod] public void NoLock() { @@ -551,6 +607,255 @@ UNION ALL SELECT * FROM account WHERE accountid IN ( SELECT accountid FROM account_hierarchical )"), NormalizeWhitespace(converted)); } + [TestMethod] + public void Exists() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE EXISTS( SELECT account.primarycontactid FROM account WHERE account.statecode = '1' AND contact.contactid = account.primarycontactid )"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void In() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE contactid IN ( SELECT account.primarycontactid FROM account WHERE account.statecode = '1' )"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void MatchFirstRowUsingCrossApply() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + "; + + 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)); + } + + [TestMethod] + public void ColumnComparison() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT firstname FROM contact WHERE firstname = lastname"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void ColumnComparisonCrossTable() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + + "; + + 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)); + } + + [TestMethod] + public void FilterLinkEntityAny() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE (EXISTS( SELECT account.primarycontactid FROM account WHERE account.name = 'Contoso' AND contact.contactid = account.primarycontactid ) OR statecode = '1')"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void FilterLinkEntityNotAny() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE NOT EXISTS( SELECT account.primarycontactid FROM account WHERE account.name = 'Contoso' AND contact.contactid = account.primarycontactid )"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void FilterLinkEntityNotAll() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE EXISTS( SELECT account.primarycontactid FROM account WHERE account.name = 'Contoso' AND contact.contactid = account.primarycontactid )"), NormalizeWhitespace(converted)); + } + + [TestMethod] + public void FilterLinkEntityAll() + { + var metadata = new AttributeMetadataCache(_service); + var fetch = @" + + + + + + + + + + + + "; + + var converted = FetchXml2Sql.Convert(_service, metadata, fetch, new FetchXml2SqlOptions { ConvertFetchXmlOperatorsTo = FetchXmlOperatorConversion.SqlCalculations }, out _); + + Assert.AreEqual(NormalizeWhitespace(@" + SELECT fullname FROM contact WHERE EXISTS( SELECT account.primarycontactid FROM account WHERE contact.contactid = account.primarycontactid ) AND NOT EXISTS( SELECT account.primarycontactid FROM account WHERE account.name = 'Contoso' AND contact.contactid = account.primarycontactid )"), NormalizeWhitespace(converted)); + } + private static string NormalizeWhitespace(string s) { return Regex.Replace(s, "\\s+", " ").Trim(); 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/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index f71493e1..c3e1e77a 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Windows.Controls.Primitives; using System.Xml.Serialization; +using Dapper; using FakeItEasy; using FakeXrmEasy; using FakeXrmEasy.FakeMessageExecutors; @@ -46,7 +47,7 @@ public void SelectArithmetic() } }; - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = query; @@ -67,7 +68,7 @@ public void SelectArithmetic() [TestMethod] public void SelectParameters() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT @param1, @param2"; @@ -88,7 +89,7 @@ public void SelectParameters() [TestMethod] public void InsertRecordsAffected() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES (@name)"; @@ -104,7 +105,7 @@ public void InsertRecordsAffected() [TestMethod] public void InsertRecordsAffectedMultipleCommands() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES (@name); INSERT INTO account (name) VALUES (@name)"; @@ -120,7 +121,7 @@ public void InsertRecordsAffectedMultipleCommands() [TestMethod] public void CombinedInsertSelect() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES (@name); SELECT accountid FROM account WHERE name = @name"; @@ -142,7 +143,7 @@ public void CombinedInsertSelect() [TestMethod] public void MultipleResultSets() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES (@name); SELECT accountid FROM account WHERE name = @name; SELECT name FROM account"; @@ -172,7 +173,7 @@ public void MultipleResultSets() [TestMethod] public void GetLastInsertId() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES (@name); SELECT @@IDENTITY"; @@ -189,7 +190,7 @@ public void GetLastInsertId() [TestMethod] public void RowCount() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES ('1'), ('2'), ('3'); SELECT @@ROWCOUNT; SELECT @@ROWCOUNT"; @@ -212,7 +213,7 @@ public void RowCount() [TestMethod] public void LoadToDataTable() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT 1, 'hello world'"; @@ -233,7 +234,7 @@ public void LoadToDataTable() [TestMethod] public void ControlOfFlow() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -276,7 +277,7 @@ SELECT @param1 [TestMethod] public void Print() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "PRINT @param1"; @@ -294,7 +295,7 @@ public void Print() [TestMethod] public void GoTo() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -339,7 +340,7 @@ goto label2 [TestMethod] public void ContinueBreak() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -382,7 +383,7 @@ select @param1 [TestMethod] public void GlobalVariablesPreservedBetweenCommands() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES ('test')"; @@ -402,7 +403,7 @@ public void GlobalVariablesPreservedBetweenCommands() [TestMethod] public void CaseInsensitiveDml() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (Name) VALUES ('ProperCase')"; @@ -438,7 +439,7 @@ public void ExecuteReaderSchemaOnly() } }; - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT name FROM account"; @@ -475,7 +476,7 @@ public void ExecuteReaderSingleRow() } }; - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT name FROM account; SELECT accountid FROM account"; @@ -510,7 +511,7 @@ public void ExecuteReaderSingleResult() } }; - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT name FROM account; SELECT accountid FROM account"; @@ -529,7 +530,7 @@ public void ExecuteReaderSingleResult() [TestMethod] public void StringLengthInSchema() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT accountid, name, name + 'foo', employees, left(name, 2) FROM account"; @@ -555,7 +556,7 @@ public void StringLengthInSchema() [TestMethod] public void StringLengthUnion() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -579,7 +580,7 @@ UNION ALL [TestMethod] public void DecimalPrecisionScaleUnion() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -611,7 +612,7 @@ UNION ALL public void DecimalPrecisionScaleCalculations() { // Examples from https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-ver15#examples - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -651,7 +652,7 @@ public void DecimalPrecisionScaleCalculations() [TestMethod] public void DefaultStringLengths() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -673,7 +674,7 @@ declare @long varchar(100) = 'this is a very long test string that is bigger tha [TestMethod] public void DateTypeConversions() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -723,7 +724,7 @@ public void DateTypeConversions() [TestMethod] public void InsertNull() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO contact (firstname, lastname, parentcustomerid) VALUES (NULL, NULL, NULL)"; @@ -743,7 +744,7 @@ public void InsertNull() [TestMethod] public void UpdateNull() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name, employees) VALUES ('Data8', 100); SELECT @@IDENTITY"; @@ -779,7 +780,7 @@ public void UpdateNull() [TestMethod] public void WaitFor() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO contact (firstname, lastname) VALUES ('Mark', 'Carrington');"; @@ -804,7 +805,7 @@ public void WaitFor() [TestMethod] public void StoredProcedureCommandType() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SampleMessage"; @@ -838,7 +839,7 @@ public void StoredProcedureCommandType() [TestMethod] public void AliasedTVF() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT msg.* FROM SampleMessage('1') AS msg"; @@ -857,7 +858,7 @@ public void AliasedTVF() [TestMethod] public void CorrelatedNotExistsTypeConversion() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT * FROM (VALUES ('1'), ('2')) a (s) WHERE NOT EXISTS (SELECT TOP 1 1 FROM (VALUES (1)) b (i) WHERE a.s = b.i)"; @@ -877,7 +878,7 @@ public void CharAscii() { // Using example from // https://docs.microsoft.com/en-us/sql/t-sql/functions/char-transact-sql?view=sql-server-ver16#a-using-ascii-and-char-to-print-ascii-values-from-a-string - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -922,7 +923,7 @@ public void NCharUnicode() { // Using example from // https://docs.microsoft.com/en-us/sql/t-sql/functions/nchar-transact-sql?view=sql-server-ver16#b-using-substring-unicode-convert-and-nchar - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -990,7 +991,7 @@ public void NCharUnicode() [ExpectedException(typeof(Sql4CdsException))] public void Timeout() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandTimeout = 1; @@ -1007,7 +1008,7 @@ public void Timeout() [TestMethod] public void ReusedParameter() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1036,7 +1037,7 @@ INSERT INTO account (name) VALUES (@name) [TestMethod] public void SortByCollation() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES ('Chiapas'),('Colima'), ('Cinco Rios'), ('California')"; @@ -1077,7 +1078,7 @@ public void SortByCollation() [TestMethod] public void CollationSensitiveFunctions() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "select case when 'test' like 't%' then 1 else 0 end"; @@ -1141,15 +1142,17 @@ DECLARE @x xml } } - [TestMethod] - public void XmlValue() + [DataTestMethod] + [DataRow("/Root/ProductDescription/@ProductID", "int", 1)] + [DataRow("/Root/ProductDescription/Features/Description", "int", null)] + public void XmlValue(string xpath, string type, object expected) { using (var con = new Sql4CdsConnection(_dataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandTimeout = 0; - cmd.CommandText = @"DECLARE @myDoc XML + cmd.CommandText = $@"DECLARE @myDoc XML DECLARE @ProdID INT SET @myDoc = ' @@ -1161,12 +1164,12 @@ DECLARE @ProdID INT ' -SET @ProdID = @myDoc.value('/Root/ProductDescription/@ProductID', 'int') +SET @ProdID = @myDoc.value('{xpath}', '{type}') SELECT @ProdID"; var actual = cmd.ExecuteScalar(); - Assert.AreEqual(1, actual); + Assert.AreEqual(expected ?? DBNull.Value, actual); } } @@ -1278,7 +1281,7 @@ SELECT 2 + 2 public void UpdateCase() { // https://github.com/MarkMpn/Sql4Cds/issues/314 - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandTimeout = 0; @@ -1298,7 +1301,7 @@ public void UpdateCase() [TestMethod] public void FullOuterJoinNoEqijoinPredicate() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandTimeout = 0; @@ -1336,7 +1339,7 @@ public void FullOuterJoinNoEqijoinPredicate() [TestMethod] public void StringAgg() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES ('A')"; @@ -1357,7 +1360,7 @@ public void StringAgg() [TestMethod] public void VariantType() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { // Can select two variant values of different types in the same column @@ -1377,7 +1380,7 @@ public void VariantType() [TestMethod] public void VariantComparisons() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { // Variant values are compared according to the type family hierarchy @@ -1413,7 +1416,7 @@ select sql_variant_property(@v, 'BaseType') as BaseType, -- 'decimal', [TestMethod] public void SqlVariantProperty() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { // Variant values are compared according to the type family hierarchy @@ -1443,7 +1446,7 @@ SELECT SQL_VARIANT_PROPERTY(@v,'BaseType') AS 'Base Type', [TestMethod] public void VariantTypes() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { // Variant values are compared according to the type family hierarchy @@ -1487,7 +1490,7 @@ UNION ALL [TestMethod] public void ExecSetState() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO contact (firstname, lastname) VALUES ('Test', 'User'); SELECT @@IDENTITY"; @@ -1516,7 +1519,7 @@ DECLARE @id EntityReference [TestMethod] public void ComplexFetchXmlAlias() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "INSERT INTO account (name) VALUES ('Data8')"; @@ -1537,7 +1540,7 @@ public void ComplexFetchXmlAlias() [TestMethod] public void CheckForMissingTable() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1553,7 +1556,7 @@ IF EXISTS(SELECT * FROM metadata.entity WHERE logicalname = 'missing') [TestMethod] public void Throw() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "THROW 51000, 'The record does not exist.', 1;"; @@ -1579,7 +1582,7 @@ public void Throw() [TestMethod] public void Catch() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1609,7 +1612,7 @@ SELECT ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(), ERROR [TestMethod] public void RaiseError() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1639,7 +1642,7 @@ SELECT ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(), ERROR [TestMethod] public void NestedCatch() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1702,7 +1705,7 @@ END CATCH [TestMethod] public void GotoOutOfCatchBlockClearsError() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1758,7 +1761,7 @@ END CATCH [TestMethod] public void Rethrow() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -1812,7 +1815,7 @@ BEGIN CATCH [DataRow("SELECT FORMATMESSAGE('Hello %-20s!', 'TEST')", "Hello TEST !")] public void FormatMessage(string query, string expected) { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = query; @@ -1832,7 +1835,7 @@ public void ConversionErrors(string column, string type, int expectedError) { var tableName = column.StartsWith("new_") ? "new_customentity" : "account"; - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { var accountId = Guid.NewGuid(); @@ -1892,7 +1895,7 @@ public void ConversionErrors(string column, string type, int expectedError) public void MetadataGuidConversionErrors() { // Failures converting string to guid should be handled in the same way for metadata queries as for FetchXML - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT logicalname FROM metadata.entity WHERE metadataid = 'test'"; @@ -1914,7 +1917,7 @@ public void MetadataGuidConversionErrors() public void MetadataEnumConversionErrors() { // Enum values are presented as simple strings, so there should be no error when converting invalid values - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT logicalname FROM metadata.entity WHERE ownershiptype = 'test'"; @@ -1926,5 +1929,125 @@ public void MetadataEnumConversionErrors() } } } + + class Account + { + public TId AccountId { get; set; } + public string Name { get; set; } + public int? Employees { get; set; } + } + + class EntityReferenceTypeHandler : SqlMapper.TypeHandler + { + public override EntityReference Parse(object value) + { + if (value is SqlEntityReference ser) + return ser; + + throw new NotSupportedException(); + } + + public override void SetValue(IDbDataParameter parameter, EntityReference value) + { + parameter.Value = (SqlEntityReference)value; + } + } + + [TestMethod] + public void DapperQueryEntityReference() + { + // reader.GetValue() returns a SqlEntityReference value - need a custom type handler to convert it to the EntityReference + // property type + SqlMapper.AddTypeHandler(new EntityReferenceTypeHandler()); + + DapperQuery(id => id.Id); + } + + [TestMethod] + public void DapperQuerySqlEntityReference() + { + DapperQuery(id => id.Id); + } + + [TestMethod] + public void DapperQueryGuid() + { + DapperQuery(id => id); + } + + private void DapperQuery(Func selector) + { + using (var con = new Sql4CdsConnection(_localDataSources)) + { + if (typeof(TId) == typeof(Guid)) + con.ReturnEntityReferenceAsGuid = true; + + var accountId1 = Guid.NewGuid(); + var accountId2 = Guid.NewGuid(); + _context.Data["account"] = new Dictionary + { + [accountId1] = new Entity("account", accountId1) + { + ["accountid"] = accountId1, + ["name"] = "Account 1", + ["employees"] = 10, + ["createdon"] = DateTime.Now, + ["turnover"] = new Money(1_000_000), + ["address1_latitude"] = 45.0D + }, + [accountId2] = new Entity("account", accountId2) + { + ["accountid"] = accountId2, + ["name"] = "Account 2", + ["createdon"] = DateTime.Now, + ["turnover"] = new Money(1_000_000), + ["address1_latitude"] = 45.0D + } + }; + + var accounts = con.Query>("SELECT accountid, name, employees FROM account").AsList(); + Assert.AreEqual(2, accounts.Count); + var account1 = accounts.Single(a => selector(a.AccountId) == accountId1); + var account2 = accounts.Single(a => selector(a.AccountId) == accountId2); + Assert.AreEqual("Account 1", account1.Name); + Assert.AreEqual("Account 2", account2.Name); + Assert.AreEqual(10, account1.Employees); + Assert.IsNull(account2.Employees); + } + } + + class SqlEntityReferenceTypeHandler : SqlMapper.TypeHandler + { + public override SqlEntityReference Parse(object value) + { + if (value is SqlEntityReference ser) + return ser; + + throw new NotSupportedException(); + } + + public override void SetValue(IDbDataParameter parameter, SqlEntityReference value) + { + parameter.Value = value; + } + } + + [TestMethod] + public void DapperParameters() + { + // Dapper wants to set the DbType of parameters but doesn't understand the SqlEntityReference type, need a custom + // type handler to set the paramete + SqlMapper.AddTypeHandler(new SqlEntityReferenceTypeHandler()); + + using (var con = new Sql4CdsConnection(_localDataSources)) + { + con.Execute("INSERT INTO account (name) VALUES (@name)", new { name = "Dapper" }); + var id = con.ExecuteScalar("SELECT @@IDENTITY"); + + var name = con.ExecuteScalar("SELECT name FROM account WHERE accountid = @id", new { id }); + + Assert.AreEqual("Dapper", name); + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs index a30f218e..f1c51508 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/CteTests.cs @@ -28,12 +28,6 @@ namespace MarkMpn.Sql4Cds.Engine.Tests [TestClass] public class CteTests : FakeXrmEasyTestsBase, IQueryExecutionOptions { - private List _supportedJoins = new List - { - JoinOperator.Inner, - JoinOperator.LeftOuter - }; - CancellationToken IQueryExecutionOptions.CancellationToken => CancellationToken.None; bool IQueryExecutionOptions.BlockUpdateWithoutWhere => false; @@ -48,12 +42,8 @@ public class CteTests : FakeXrmEasyTestsBase, IQueryExecutionOptions int IQueryExecutionOptions.MaxDegreeOfParallelism => 10; - bool IQueryExecutionOptions.ColumnComparisonAvailable => true; - bool IQueryExecutionOptions.UseLocalTimeZone => true; - List IQueryExecutionOptions.JoinOperatorsAvailable => _supportedJoins; - bool IQueryExecutionOptions.BypassCustomPlugins => false; void IQueryExecutionOptions.ConfirmInsert(ConfirmDmlStatementEventArgs e) @@ -88,7 +78,7 @@ void IQueryExecutionOptions.Progress(double? progress, string message) [TestMethod] public void SimpleSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS (SELECT accountid, name FROM account) @@ -112,7 +102,7 @@ WITH cte AS (SELECT accountid, name FROM account) [TestMethod] public void ColumnAliases() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte (id, n) AS (SELECT accountid, name FROM account) @@ -136,7 +126,7 @@ WITH cte (id, n) AS (SELECT accountid, name FROM account) [TestMethod] public void MultipleAnchorQueries() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte (id, n) AS (SELECT accountid, name FROM account UNION ALL select contactid, fullname FROM contact) @@ -169,7 +159,7 @@ WITH cte (id, n) AS (SELECT accountid, name FROM account UNION ALL select contac [TestMethod] public void MergeFilters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname = 'Mark') @@ -198,7 +188,7 @@ WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname [TestMethod] public void MultipleReferencesWithAliases() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname = 'Mark') @@ -236,7 +226,7 @@ WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname [TestMethod] public void MultipleReferencesInUnionAll() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname = 'Mark') @@ -272,7 +262,7 @@ WITH cte AS (SELECT contactid, firstname, lastname FROM contact WHERE firstname [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void MultipleRecursiveReferences() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -289,7 +279,7 @@ UNION ALL [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void HintsOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -306,7 +296,7 @@ UNION ALL [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void RecursionWithoutUnionAll() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -323,7 +313,7 @@ SELECT cte.* FROM cte [ExpectedException(typeof(QueryParseException))] public void OrderByWithoutTop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -341,7 +331,7 @@ ORDER BY firstname [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void GroupByOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -358,7 +348,7 @@ UNION ALL [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void AggregateOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -375,7 +365,7 @@ SELECT MIN(contactid), MIN(firstname), MIN(lastname) FROM cte [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void TopOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -392,7 +382,7 @@ SELECT TOP 10 cte.* FROM cte [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void OuterJoinOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -409,7 +399,7 @@ UNION ALL [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void SubqueryOnRecursiveReference() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -426,7 +416,7 @@ UNION ALL [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void IncorrectColumnCount() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte (id, fname) AS ( @@ -441,7 +431,7 @@ WITH cte (id, fname) AS ( [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void AnonymousColumn() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -456,7 +446,7 @@ WITH cte AS ( [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void MissingAnchorQuery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte (x, y) AS ( @@ -470,7 +460,7 @@ WITH cte (x, y) AS ( [TestMethod] public void AliasedAnonymousColumn() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte (id, fname, lname) AS ( @@ -508,7 +498,7 @@ WITH cte (id, fname, lname) AS ( [TestMethod] public void SelectStarFromValues() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH source_data_cte AS ( @@ -545,7 +535,7 @@ WITH source_data_cte AS ( [TestMethod] public void SimpleRecursion() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH cte AS ( @@ -614,7 +604,7 @@ UNION ALL [TestMethod] public void FactorialCalc() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -647,7 +637,7 @@ UNION ALL [TestMethod] public void FactorialCalcFiltered() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -664,7 +654,7 @@ UNION ALL [TestMethod] public void FactorialCalcFilteredCaseInsensitive() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -682,7 +672,7 @@ union all [TestMethod] public void Under() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH account_hierarchical(accountid) AS ( @@ -713,7 +703,7 @@ UNION ALL [TestMethod] public void EqOrUnder() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH account_hierarchical(accountid) AS ( @@ -744,7 +734,7 @@ UNION ALL [TestMethod] public void Above() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH account_hierarchical(accountid, parentaccountid) AS ( @@ -775,7 +765,7 @@ UNION ALL [TestMethod] public void EqOrAbove() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" WITH account_hierarchical(accountid, parentaccountid) AS ( diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs index 3db165ef..09c12097 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanNodeTests.cs @@ -37,7 +37,7 @@ public void ConstantScanTest() Alias = "test" }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(1, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["test.firstname"]).Value); @@ -84,7 +84,7 @@ public void FilterNodeTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(1, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["test.firstname"]).Value); @@ -161,7 +161,7 @@ public void MergeJoinInnerTest() JoinType = QualifiedJoinType.Inner }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(2, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["f.firstname"]).Value); @@ -241,7 +241,7 @@ public void MergeJoinLeftOuterTest() JoinType = QualifiedJoinType.LeftOuter }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(3, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["f.firstname"]).Value); @@ -323,7 +323,7 @@ public void MergeJoinRightOuterTest() JoinType = QualifiedJoinType.RightOuter }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(3, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["f.firstname"]).Value); @@ -362,7 +362,7 @@ public void AssertionTest() ErrorMessage = "Only Mark is allowed" }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).GetEnumerator(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).GetEnumerator(); Assert.IsTrue(results.MoveNext()); Assert.AreEqual("Mark", results.Current.GetAttributeValue("test.name").Value); @@ -420,7 +420,7 @@ public void ComputeScalarTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("mul").Value) .ToArray(); @@ -462,7 +462,7 @@ public void DistinctTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.value1").Value) .ToArray(); @@ -504,7 +504,7 @@ public void DistinctCaseInsensitiveTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.value1").Value) .ToArray(); @@ -562,7 +562,7 @@ public void SortNodeTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.expectedorder").Value) .ToArray(); @@ -627,7 +627,7 @@ public void SortNodePresortedTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.expectedorder").Value) .ToArray(); @@ -659,11 +659,11 @@ public void TableSpoolTest() var spool = new TableSpoolNode { Source = source }; - var results1 = spool.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results1 = spool.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.value1").Value) .ToArray(); - var results2 = spool.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results2 = spool.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => e.GetAttributeValue("test.value1").Value) .ToArray(); @@ -708,7 +708,7 @@ public void CaseInsenstiveHashMatchAggregateNodeTest() } }; - var results = spool.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)) + var results = spool.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)) .Select(e => new { Name = e.GetAttributeValue("src.value1").Value, Count = e.GetAttributeValue("count").Value }) .ToArray(); @@ -749,7 +749,7 @@ public void SqlTransformSingleResult() public void AggregateInitialTest() { var aggregate = CreateAggregateTest(); - var result = aggregate.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).Single(); + var result = aggregate.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).Single(); Assert.AreEqual(SqlInt32.Null, result["min"]); Assert.AreEqual(SqlInt32.Null, result["max"]); @@ -767,7 +767,7 @@ public void AggregateInitialTest() public void AggregateSingleValueTest() { var aggregate = CreateAggregateTest(1); - var result = aggregate.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).Single(); + var result = aggregate.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).Single(); Assert.AreEqual((SqlInt32)1, result["min"]); Assert.AreEqual((SqlInt32)1, result["max"]); @@ -785,7 +785,7 @@ public void AggregateSingleValueTest() public void AggregateTwoEqualValuesTest() { var aggregate = CreateAggregateTest(1, 1); - var result = aggregate.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).Single(); + var result = aggregate.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).Single(); Assert.AreEqual((SqlInt32)1, result["min"]); Assert.AreEqual((SqlInt32)1, result["max"]); @@ -803,7 +803,7 @@ public void AggregateTwoEqualValuesTest() public void AggregateMultipleValuesTest() { var aggregate = CreateAggregateTest(1, 3, 1, 1); - var result = aggregate.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).Single(); + var result = aggregate.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).Single(); Assert.AreEqual((SqlInt32)1, result["min"]); Assert.AreEqual((SqlInt32)3, result["max"]); @@ -962,7 +962,7 @@ public void NestedLoopJoinInnerTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(2, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["f.firstname"]).Value); @@ -1034,7 +1034,7 @@ public void NestedLoopJoinLeftOuterTest() } }; - var results = node.Execute(new NodeExecutionContext(_localDataSource, new StubOptions(), null, null, null)).ToArray(); + var results = node.Execute(new NodeExecutionContext(_localDataSources, new StubOptions(), null, null, null)).ToArray(); Assert.AreEqual(3, results.Length); Assert.AreEqual("Mark", ((SqlString)results[0]["f.firstname"]).Value); diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs index 9bea3a82..ae0f4ca3 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExecutionPlanTests.cs @@ -28,12 +28,6 @@ namespace MarkMpn.Sql4Cds.Engine.Tests [TestClass] public class ExecutionPlanTests : FakeXrmEasyTestsBase, IQueryExecutionOptions { - private List _supportedJoins = new List - { - JoinOperator.Inner, - JoinOperator.LeftOuter - }; - CancellationToken IQueryExecutionOptions.CancellationToken => CancellationToken.None; bool IQueryExecutionOptions.BlockUpdateWithoutWhere => false; @@ -48,12 +42,8 @@ public class ExecutionPlanTests : FakeXrmEasyTestsBase, IQueryExecutionOptions int IQueryExecutionOptions.MaxDegreeOfParallelism => 10; - bool IQueryExecutionOptions.ColumnComparisonAvailable => true; - bool IQueryExecutionOptions.UseLocalTimeZone => true; - List IQueryExecutionOptions.JoinOperatorsAvailable => _supportedJoins; - bool IQueryExecutionOptions.BypassCustomPlugins => false; void IQueryExecutionOptions.ConfirmInsert(ConfirmDmlStatementEventArgs e) @@ -88,7 +78,7 @@ void IQueryExecutionOptions.Progress(double? progress, string message) [TestMethod] public void SimpleSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name FROM account"; @@ -110,7 +100,7 @@ public void SimpleSelect() [TestMethod] public void SimpleSelectStar() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account"; @@ -146,7 +136,7 @@ public void SimpleSelectStar() [TestMethod] public void Join() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name FROM account INNER JOIN contact ON account.accountid = contact.parentcustomerid"; @@ -170,7 +160,7 @@ public void Join() [TestMethod] public void JoinWithExtraCondition() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -203,7 +193,7 @@ public void JoinWithExtraCondition() [TestMethod] public void NonUniqueJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name FROM account INNER JOIN contact ON account.name = contact.fullname"; @@ -231,7 +221,7 @@ public void NonUniqueJoin() [TestMethod] public void NonUniqueJoinExpression() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name FROM account INNER JOIN contact ON account.name = (contact.firstname + ' ' + contact.lastname)"; @@ -266,7 +256,7 @@ public void NonUniqueJoinExpression() [TestMethod] public void SimpleWhere() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -295,10 +285,79 @@ public void SimpleWhere() "); } + [TestMethod] + public void WhereColumnComparison() + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @" + SELECT + accountid, + name + FROM + account + WHERE + name = accountid"; + + 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 WhereColumnComparisonCrossTable() + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @" + SELECT + accountid, + name, + fullname + FROM + account + INNER JOIN contact ON account.accountid = contact.parentcustomerid + WHERE + name = fullname"; + + 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 SimpleSort() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -328,7 +387,7 @@ ORDER BY [TestMethod] public void SimpleSortIndex() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -358,7 +417,7 @@ ORDER BY [TestMethod] public void SimpleDistinct() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT DISTINCT @@ -384,7 +443,7 @@ SELECT DISTINCT [TestMethod] public void SimpleTop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 @@ -411,7 +470,7 @@ SELECT TOP 10 [TestMethod] public void SimpleOffset() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -441,7 +500,7 @@ ORDER BY name [TestMethod] public void SimpleGroupAggregate() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -507,7 +566,7 @@ public void SimpleGroupAggregate() [TestMethod] public void AliasedAggregate() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -557,7 +616,7 @@ public void AliasedAggregate() [TestMethod] public void AliasedGroupingAggregate() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -607,7 +666,7 @@ public void AliasedGroupingAggregate() [TestMethod] public void SimpleAlias() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name AS test FROM account"; @@ -629,7 +688,7 @@ public void SimpleAlias() [TestMethod] public void SimpleHaving() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -695,7 +754,7 @@ GROUP BY name [TestMethod] public void GroupByDatePart() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -753,7 +812,7 @@ public void GroupByDatePart() [TestMethod] public void GroupByDatePartUsingYearMonthDayFunctions() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -832,7 +891,7 @@ GROUP BY [TestMethod] public void PartialOrdering() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -866,10 +925,50 @@ ORDER BY "); } + [TestMethod] + public void OrderByEntityName() + { + using (_localDataSource.SetOrderByEntityName(true)) + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @" + SELECT TOP 1000 + name, + firstname + FROM + account + INNER JOIN contact ON account.accountid = contact.parentcustomerid + ORDER BY + name, + firstname, + accountid"; + + 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 PartialOrderingAvoidingLegacyPagingWithTop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 100 @@ -888,17 +987,15 @@ ORDER BY Assert.AreEqual(1, plans.Length); var select = AssertNode(plans[0]); - var top = AssertNode(select.Source); - var order = AssertNode(top.Source); - var fetch = AssertNode(order.Source); + var fetch = AssertNode(select.Source); AssertFetchXml(fetch, @" - + - + @@ -908,7 +1005,7 @@ ORDER BY [TestMethod] public void PartialWhere() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -944,7 +1041,7 @@ public void PartialWhere() [TestMethod] public void ComputeScalarSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname FROM contact WHERE firstname = 'Mark'"; @@ -973,7 +1070,7 @@ public void ComputeScalarSelect() [TestMethod] public void ComputeScalarFilter() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT contactid FROM contact WHERE firstname + ' ' + lastname = 'Mark Carrington'"; @@ -999,7 +1096,7 @@ public void ComputeScalarFilter() [TestMethod] public void SelectSubqueryWithMergeJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT name FROM account WHERE accountid = parentcustomerid) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1032,7 +1129,7 @@ public void SelectSubqueryWithMergeJoin() [TestMethod] public void SelectSubqueryWithNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT name FROM account WHERE createdon = contact.createdon) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1082,7 +1179,7 @@ public void SelectSubqueryWithNestedLoop() [TestMethod] public void SelectSubqueryWithChildRecordUsesNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name, (SELECT TOP 1 fullname FROM contact WHERE parentcustomerid = account.accountid) FROM account WHERE name = 'Data8'"; @@ -1125,7 +1222,7 @@ public void SelectSubqueryWithChildRecordUsesNestedLoop() [TestMethod] public void SelectSubqueryWithSmallNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT name FROM account WHERE createdon = contact.createdon) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1170,7 +1267,7 @@ public void SelectSubqueryWithSmallNestedLoop() [TestMethod] public void SelectSubqueryWithNonCorrelatedNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT TOP 1 name FROM account) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1212,7 +1309,7 @@ public void SelectSubqueryWithNonCorrelatedNestedLoop() [TestMethod] public void SelectSubqueryWithCorrelatedSpooledNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT name FROM account WHERE createdon = contact.createdon) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1263,7 +1360,7 @@ public void SelectSubqueryWithCorrelatedSpooledNestedLoop() [TestMethod] public void SelectSubqueryWithPartiallyCorrelatedSpooledNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT name FROM account WHERE createdon = contact.createdon AND employees > 10) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1316,7 +1413,7 @@ public void SelectSubqueryWithPartiallyCorrelatedSpooledNestedLoop() public void SelectSubqueryUsingOuterReferenceInSelectClause() { var tableSize = new StubTableSizeCache(); - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname, 'Account: ' + (SELECT firstname + ' ' + name FROM account WHERE accountid = parentcustomerid) AS accountname FROM contact WHERE firstname = 'Mark'"; @@ -1365,7 +1462,7 @@ public void SelectSubqueryUsingOuterReferenceInSelectClause() [TestMethod] public void SelectSubqueryUsingOuterReferenceInOrderByClause() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname FROM contact ORDER BY (SELECT TOP 1 name FROM account WHERE accountid = parentcustomerid ORDER BY firstname)"; @@ -1405,7 +1502,7 @@ public void SelectSubqueryUsingOuterReferenceInOrderByClause() [TestMethod] public void WhereSubquery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT firstname + ' ' + lastname AS fullname FROM contact WHERE (SELECT name FROM account WHERE accountid = parentcustomerid) = 'Data8'"; @@ -1425,7 +1522,6 @@ public void WhereSubquery() - @@ -1437,7 +1533,7 @@ public void WhereSubquery() [TestMethod] public void ComputeScalarDistinct() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT DISTINCT TOP 10 @@ -1467,7 +1563,7 @@ SELECT DISTINCT TOP 10 [TestMethod] public void Union() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account @@ -1510,7 +1606,7 @@ SELECT name FROM account [TestMethod] public void UnionMultiple() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account @@ -1564,7 +1660,7 @@ SELECT fullname FROM contact [TestMethod] public void UnionSort() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account @@ -1610,7 +1706,7 @@ SELECT fullname FROM contact [TestMethod] public void UnionSortOnAlias() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name AS n FROM account @@ -1656,7 +1752,7 @@ SELECT fullname FROM contact [TestMethod] public void UnionSortOnAliasedColumnsOriginalName() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name AS n FROM account @@ -1702,7 +1798,7 @@ SELECT fullname FROM contact [TestMethod] public void UnionAll() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account @@ -1741,7 +1837,7 @@ UNION ALL [TestMethod] public void SimpleInFilter() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1776,7 +1872,7 @@ public void SimpleInFilter() [TestMethod] public void SubqueryInFilterUncorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1817,7 +1913,7 @@ public void SubqueryInFilterUncorrelated() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void SubqueryInFilterMultipleColumnsError() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1834,7 +1930,7 @@ public void SubqueryInFilterMultipleColumnsError() [TestMethod] public void SubqueryInFilterUncorrelatedPrimaryKey() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1869,7 +1965,7 @@ public void SubqueryInFilterUncorrelatedPrimaryKey() [TestMethod] public void SubqueryInFilterCorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1917,7 +2013,7 @@ public void SubqueryInFilterCorrelated() [TestMethod] public void SubqueryNotInFilterCorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -1965,7 +2061,7 @@ public void SubqueryNotInFilterCorrelated() [TestMethod] public void ExistsFilterUncorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2003,7 +2099,7 @@ public void ExistsFilterUncorrelated() [TestMethod] public void ExistsFilterCorrelatedPrimaryKey() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2035,7 +2131,7 @@ public void ExistsFilterCorrelatedPrimaryKey() [TestMethod] public void ExistsFilterCorrelatedPrimaryKeyOr() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2072,7 +2168,7 @@ public void ExistsFilterCorrelatedPrimaryKeyOr() [TestMethod] public void ExistsFilterCorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2113,10 +2209,138 @@ public void ExistsFilterCorrelated() "); } + [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(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(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(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(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(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(fetch, @" + + + + + + + + + + + + + + + "); + } + } + [TestMethod] public void NotExistsFilterCorrelated() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2151,7 +2375,7 @@ public void NotExistsFilterCorrelated() [TestMethod] public void QueryDerivedTableSimple() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 @@ -2182,7 +2406,7 @@ SELECT TOP 10 [TestMethod] public void QueryDerivedTableAlias() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 @@ -2213,7 +2437,7 @@ SELECT TOP 10 [TestMethod] public void QueryDerivedTableValues() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 @@ -2240,7 +2464,7 @@ SELECT TOP 10 [TestMethod] public void NoLockTableHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT TOP 10 @@ -2270,7 +2494,7 @@ SELECT TOP 10 [TestMethod] public void CrossJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2309,7 +2533,7 @@ CROSS JOIN [TestMethod] public void CrossApply() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2346,7 +2570,7 @@ FROM contact [TestMethod] public void CrossApplyAllColumns() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2393,7 +2617,7 @@ FROM contact [TestMethod] public void CrossApplyRestrictedColumns() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2430,7 +2654,7 @@ FROM contact [TestMethod] public void CrossApplyRestrictedColumnsWithAlias() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2474,7 +2698,7 @@ FROM contact [TestMethod] public void CrossApplyJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2525,7 +2749,7 @@ FROM contact [TestMethod] public void OuterApply() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2562,7 +2786,7 @@ FROM contact [TestMethod] public void OuterApplyNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2620,7 +2844,7 @@ ORDER BY firstname [TestMethod] public void FetchXmlNativeWhere() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -2652,7 +2876,7 @@ public void FetchXmlNativeWhere() [TestMethod] public void SimpleMetadataSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT logicalname @@ -2673,7 +2897,7 @@ SELECT logicalname [TestMethod] public void SimpleMetadataWhere() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT logicalname @@ -2699,7 +2923,7 @@ FROM metadata.entity [TestMethod] public void CaseSensitiveMetadataWhere() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT logicalname @@ -2730,7 +2954,7 @@ FROM metadata.entity [TestMethod] public void SimpleUpdate() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "UPDATE account SET name = 'foo' WHERE name = 'bar'"; @@ -2759,7 +2983,7 @@ public void SimpleUpdate() [TestMethod] public void UpdateFromJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "UPDATE a SET name = 'foo' FROM account a INNER JOIN contact c ON a.accountid = c.parentcustomerid WHERE name = 'bar'"; @@ -2791,7 +3015,7 @@ public void UpdateFromJoin() [TestMethod] public void QueryHints() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT accountid, name FROM account OPTION (OPTIMIZE FOR UNKNOWN, FORCE ORDER, RECOMPILE, USE HINT('DISABLE_OPTIMIZER_ROWGOAL'), USE HINT('ENABLE_QUERY_OPTIMIZER_HOTFIXES'), LOOP JOIN, MERGE JOIN, HASH JOIN, NO_PERFORMANCE_SPOOL, MAXRECURSION 2)"; @@ -2813,7 +3037,7 @@ public void QueryHints() [TestMethod] public void AggregateSort() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name, count(*) from account group by name order by 2 desc"; @@ -2865,7 +3089,7 @@ public void AggregateSort() [TestMethod] public void FoldFilterWithNonFoldedJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account INNER JOIN contact ON left(name, 4) = left(firstname, 4) where name like 'Data8%' and firstname like 'Mark%'"; @@ -2902,7 +3126,7 @@ public void FoldFilterWithNonFoldedJoin() [TestMethod] public void FoldFilterWithInClause() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account where name like 'Data8%' and primarycontactid in (select contactid from contact where firstname = 'Mark')"; @@ -2931,7 +3155,7 @@ public void FoldFilterWithInClause() [TestMethod] public void FoldFilterWithInClauseOr() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account where name like 'Data8%' or primarycontactid in (select contactid from contact where firstname = 'Mark')"; @@ -2961,11 +3185,9 @@ public void FoldFilterWithInClauseOr() [TestMethod] public void FoldFilterWithInClauseWithoutPrimaryKey() { - _supportedJoins.Add(JoinOperator.Any); - - try + using (_localDataSource.EnableJoinOperator(JoinOperator.In)) { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account where name like 'Data8%' and createdon in (select createdon from contact where firstname = 'Mark')"; @@ -2979,7 +3201,7 @@ public void FoldFilterWithInClauseWithoutPrimaryKey() - + @@ -2990,10 +3212,6 @@ public void FoldFilterWithInClauseWithoutPrimaryKey() "); } - finally - { - _supportedJoins.Remove(JoinOperator.Any); - } } [TestMethod] @@ -3030,11 +3248,9 @@ public void FoldNotInToLeftOuterJoin() [TestMethod] public void FoldFilterWithInClauseOnLinkEntityWithoutPrimaryKey() { - _supportedJoins.Add(JoinOperator.Any); - - try + using (_localDataSource.EnableJoinOperator(JoinOperator.In)) { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account inner join contact on account.accountid = contact.parentcustomerid where name like 'Data8%' and contact.createdon in (select createdon from contact where firstname = 'Mark')"; @@ -3053,7 +3269,7 @@ public void FoldFilterWithInClauseOnLinkEntityWithoutPrimaryKey() - + @@ -3061,20 +3277,14 @@ public void FoldFilterWithInClauseOnLinkEntityWithoutPrimaryKey() "); } - finally - { - _supportedJoins.Remove(JoinOperator.Any); - } } [TestMethod] public void FoldFilterWithExistsClauseWithoutPrimaryKey() { - _supportedJoins.Add(JoinOperator.Exists); - - try + using (_localDataSource.EnableJoinOperator(JoinOperator.Exists)) { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name from account where name like 'Data8%' and exists (select * from contact where firstname = 'Mark' and createdon = account.createdon)"; @@ -3088,7 +3298,7 @@ public void FoldFilterWithExistsClauseWithoutPrimaryKey() - + @@ -3100,16 +3310,12 @@ public void FoldFilterWithExistsClauseWithoutPrimaryKey() "); } - finally - { - _supportedJoins.Remove(JoinOperator.Exists); - } } [TestMethod] public void DistinctNotRequiredWithPrimaryKey() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT accountid, name from account"; @@ -3131,7 +3337,7 @@ public void DistinctNotRequiredWithPrimaryKey() [TestMethod] public void DistinctRequiredWithoutPrimaryKey() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT accountid, name from account INNER JOIN contact ON account.accountid = contact.parentcustomerid"; @@ -3157,7 +3363,7 @@ public void DistinctRequiredWithoutPrimaryKey() [TestMethod] public void SimpleDelete() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "DELETE FROM account WHERE name = 'bar'"; @@ -3183,7 +3389,7 @@ public void SimpleDelete() [TestMethod] public void SimpleInsertSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "INSERT INTO account (name) SELECT fullname FROM contact WHERE firstname = 'Mark'"; @@ -3209,7 +3415,7 @@ public void SimpleInsertSelect() [TestMethod] public void SelectDuplicateColumnNames() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT fullname, lastname + ', ' + firstname as fullname FROM contact WHERE firstname = 'Mark'"; @@ -3242,7 +3448,7 @@ public void SelectDuplicateColumnNames() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void SubQueryDuplicateColumnNamesError() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM (SELECT fullname, lastname + ', ' + firstname as fullname FROM contact WHERE firstname = 'Mark') a"; @@ -3252,7 +3458,7 @@ public void SubQueryDuplicateColumnNamesError() [TestMethod] public void UnionDuplicateColumnNames() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT fullname, lastname + ', ' + firstname as fullname FROM contact WHERE firstname = 'Mark' UNION @@ -3283,7 +3489,7 @@ public void UnionDuplicateColumnNames() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void SubQueryUnionDuplicateColumnNamesError() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT * FROM ( SELECT fullname, lastname + ', ' + firstname as fullname FROM contact WHERE firstname = 'Mark' UNION @@ -3319,7 +3525,7 @@ private void AssertFetchXml(FetchXmlScan node, string fetchXml) [TestMethod] public void SelectStarInSubquery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT * FROM account WHERE accountid IN (SELECT parentcustomerid FROM contact)"; @@ -3359,7 +3565,7 @@ public void SelectStarInSubquery() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void CannotSelectColumnsFromSemiJoin() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT contact.* FROM account WHERE accountid IN (SELECT parentcustomerid FROM contact)"; @@ -3369,7 +3575,7 @@ public void CannotSelectColumnsFromSemiJoin() [TestMethod] public void MinAggregateNotFoldedToFetchXmlForOptionset() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT new_name, min(new_optionsetvalue) FROM new_customentity GROUP BY new_name"; @@ -3394,7 +3600,7 @@ public void MinAggregateNotFoldedToFetchXmlForOptionset() [TestMethod] public void HelpfulErrorMessageOnMissingGroupBy() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT new_name, min(new_optionsetvalue) FROM new_customentity"; @@ -3412,7 +3618,7 @@ public void HelpfulErrorMessageOnMissingGroupBy() [TestMethod] public void AggregateInSubquery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact @@ -3508,7 +3714,7 @@ GROUP BY firstname }, }; - var result = select.Execute(new NodeExecutionContext(_localDataSource, this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); + var result = select.Execute(new NodeExecutionContext(_localDataSources, this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); var dataTable = new DataTable(); dataTable.Load(result); @@ -3520,7 +3726,7 @@ GROUP BY firstname [TestMethod] public void SelectVirtualNameAttributeFromLinkEntity() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT parentcustomeridname FROM account INNER JOIN contact ON account.accountid = contact.parentcustomerid"; @@ -3543,7 +3749,7 @@ public void SelectVirtualNameAttributeFromLinkEntity() [TestMethod] public void DuplicatedDistinctColumns() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT name AS n1, name AS n2 FROM account"; @@ -3565,7 +3771,7 @@ public void DuplicatedDistinctColumns() [TestMethod] public void GroupByDatetimeWithoutDatePart() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT createdon, COUNT(*) FROM account GROUP BY createdon"; @@ -3588,7 +3794,7 @@ public void GroupByDatetimeWithoutDatePart() [TestMethod] public void MetadataExpressions() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT collectionschemaname + '.' + entitysetname FROM metadata.entity WHERE description LIKE '%test%'"; @@ -3613,7 +3819,7 @@ public void MetadataExpressions() [TestMethod] public void AliasedAttribute() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name AS n1 FROM account WHERE name = 'test'"; @@ -3640,7 +3846,7 @@ public void AliasedAttribute() [TestMethod] public void MultipleAliases() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name AS n1, name AS n2 FROM account WHERE name = 'test'"; @@ -3743,7 +3949,7 @@ public void CrossInstanceJoin() [TestMethod] public void FilterOnGroupByExpression() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -3787,7 +3993,7 @@ GROUP BY [TestMethod] public void SystemFunctions() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT CURRENT_TIMESTAMP, CURRENT_USER, GETDATE(), USER_NAME()"; @@ -3803,7 +4009,7 @@ public void SystemFunctions() [TestMethod] public void FoldEqualsCurrentUser() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name FROM account WHERE ownerid = CURRENT_USER"; @@ -3827,7 +4033,7 @@ public void FoldEqualsCurrentUser() [TestMethod] public void EntityReferenceInQuery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name FROM account WHERE accountid IN ('0000000000000000-0000-0000-000000000000', '0000000000000000-0000-0000-000000000001')"; @@ -3854,7 +4060,7 @@ public void EntityReferenceInQuery() [TestMethod] public void OrderBySelectExpression() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name + 'foo' FROM account ORDER BY 1"; @@ -3880,7 +4086,7 @@ public void OrderBySelectExpression() [TestMethod] public void OrderByAlias() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name AS companyname FROM account ORDER BY companyname"; @@ -3905,7 +4111,7 @@ public void OrderByAlias() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void OrderByAliasCantUseExpression() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name AS companyname FROM account ORDER BY companyname + ''"; @@ -3915,7 +4121,7 @@ public void OrderByAliasCantUseExpression() [TestMethod] public void DistinctOrderByUsesScalarAggregate() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT account.accountid FROM metadata.entity INNER JOIN account ON entity.metadataid = account.accountid"; @@ -3945,7 +4151,7 @@ public void DistinctOrderByUsesScalarAggregate() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void WindowFunctionsNotSupported() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT COUNT(accountid) OVER(PARTITION BY accountid) AS test FROM account"; @@ -3955,7 +4161,7 @@ public void WindowFunctionsNotSupported() [TestMethod] public void DeclareVariableSetLiteralSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test int @@ -4010,7 +4216,7 @@ DECLARE @test int [TestMethod] public void SetVariableInDeclaration() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test int = 1 @@ -4065,7 +4271,7 @@ public void SetVariableInDeclaration() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void UnknownVariable() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SET @test = 1"; @@ -4076,7 +4282,7 @@ public void UnknownVariable() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void DuplicateVariable() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test INT @@ -4088,7 +4294,7 @@ DECLARE @test INT [TestMethod] public void VariableTypeConversionIntToString() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test varchar(3) @@ -4104,7 +4310,7 @@ DECLARE @test varchar(3) { if (plan is IDataReaderExecutionPlanNode selectQuery) { - var results = selectQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), CommandBehavior.Default); + var results = selectQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), CommandBehavior.Default); var dataTable = new DataTable(); dataTable.Load(results); @@ -4114,7 +4320,7 @@ DECLARE @test varchar(3) } else if (plan is IDmlQueryExecutionPlanNode dmlQuery) { - dmlQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), out _, out _); + dmlQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), out _, out _); } } } @@ -4122,7 +4328,7 @@ DECLARE @test varchar(3) [TestMethod] public void VariableTypeConversionStringTruncation() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test varchar(3) @@ -4138,7 +4344,7 @@ DECLARE @test varchar(3) { if (plan is IDataReaderExecutionPlanNode selectQuery) { - var results = selectQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), CommandBehavior.Default); + var results = selectQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), CommandBehavior.Default); var dataTable = new DataTable(); dataTable.Load(results); @@ -4148,7 +4354,7 @@ DECLARE @test varchar(3) } else if (plan is IDmlQueryExecutionPlanNode dmlQuery) { - dmlQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), out _, out _); + dmlQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), out _, out _); } } } @@ -4157,7 +4363,7 @@ DECLARE @test varchar(3) [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void CannotCombineSetVariableAndDataRetrievalInSelect() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); // A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations var query = @" @@ -4170,7 +4376,7 @@ DECLARE @test varchar(3) [TestMethod] public void SetVariableWithSelectUsesFinalValue() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test varchar(3) @@ -4230,7 +4436,7 @@ DECLARE @test varchar(3) { if (plan is IDataReaderExecutionPlanNode selectQuery) { - var results = selectQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), CommandBehavior.Default); + var results = selectQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), CommandBehavior.Default); var dataTable = new DataTable(); dataTable.Load(results); @@ -4240,7 +4446,7 @@ DECLARE @test varchar(3) } else if (plan is IDmlQueryExecutionPlanNode dmlQuery) { - dmlQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), out _, out _); + dmlQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), out _, out _); } } } @@ -4248,7 +4454,7 @@ DECLARE @test varchar(3) [TestMethod] public void VarCharLengthDefaultsTo1() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test varchar @@ -4264,7 +4470,7 @@ DECLARE @test varchar { if (plan is IDataReaderExecutionPlanNode selectQuery) { - var results = selectQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), CommandBehavior.Default); + var results = selectQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), CommandBehavior.Default); var dataTable = new DataTable(); dataTable.Load(results); @@ -4274,7 +4480,7 @@ DECLARE @test varchar } else if (plan is IDmlQueryExecutionPlanNode dmlQuery) { - dmlQuery.Execute(new NodeExecutionContext(_localDataSource, this, parameterTypes, parameterValues, null), out _, out _); + dmlQuery.Execute(new NodeExecutionContext(_localDataSources, this, parameterTypes, parameterValues, null), out _, out _); } } } @@ -4283,7 +4489,7 @@ DECLARE @test varchar [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void CursorVariableNotSupported() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test CURSOR"; @@ -4295,7 +4501,7 @@ public void CursorVariableNotSupported() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void TableVariableNotSupported() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" DECLARE @test TABLE (ID INT)"; @@ -4306,7 +4512,7 @@ public void TableVariableNotSupported() [TestMethod] public void IfStatement() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this) { EstimatedPlanOnly = true }; + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this) { EstimatedPlanOnly = true }; var query = @" IF @param1 = 1 @@ -4340,7 +4546,7 @@ INSERT INTO account (name) VALUES ('one') [TestMethod] public void WhileStatement() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this) { EstimatedPlanOnly = true }; + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this) { EstimatedPlanOnly = true }; var query = @" WHILE @param1 < 10 @@ -4369,7 +4575,7 @@ INSERT INTO account (name) VALUES (@param1) [TestMethod] public void IfNotExists() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this) { EstimatedPlanOnly = true }; + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this) { EstimatedPlanOnly = true }; var query = @" IF NOT EXISTS(SELECT * FROM account WHERE name = @param1) @@ -4394,7 +4600,7 @@ INSERT INTO account (name) VALUES (@param1) [TestMethod] public void DuplicatedAliases() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT name, createdon AS name FROM account"; @@ -4416,7 +4622,7 @@ public void DuplicatedAliases() [TestMethod] public void MetadataLeftJoinData() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT entity.logicalname, account.name, contact.firstname @@ -4455,7 +4661,7 @@ public void MetadataLeftJoinData() [TestMethod] public void NotEqualExcludesNull() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account WHERE name <> 'Data8'"; @@ -4481,7 +4687,7 @@ public void NotEqualExcludesNull() [TestMethod] public void DistinctFromAllowsNull() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT name FROM account WHERE name IS DISTINCT FROM 'Data8'"; @@ -4506,7 +4712,7 @@ public void DistinctFromAllowsNull() [TestMethod] public void DoNotFoldFilterOnNameVirtualAttributeWithTooManyJoins() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" select top 10 a.name @@ -4599,7 +4805,7 @@ from account a [TestMethod] public void FilterOnVirtualTypeAttributeEquals() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype = 'contact'"; @@ -4623,7 +4829,7 @@ public void FilterOnVirtualTypeAttributeEquals() [TestMethod] public void FilterOnVirtualTypeAttributeEqualsImpossible() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype = 'non-existent-entity'"; @@ -4647,7 +4853,7 @@ public void FilterOnVirtualTypeAttributeEqualsImpossible() [TestMethod] public void FilterOnVirtualTypeAttributeNotEquals() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype <> 'contact'"; @@ -4672,7 +4878,7 @@ public void FilterOnVirtualTypeAttributeNotEquals() [TestMethod] public void FilterOnVirtualTypeAttributeNotInImpossible() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype NOT IN ('account', 'contact')"; @@ -4699,7 +4905,7 @@ public void FilterOnVirtualTypeAttributeNotInImpossible() [TestMethod] public void FilterOnVirtualTypeAttributeNull() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype IS NULL"; @@ -4723,7 +4929,7 @@ public void FilterOnVirtualTypeAttributeNull() [TestMethod] public void FilterOnVirtualTypeAttributeNotNull() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT firstname FROM contact WHERE parentcustomeridtype IS NOT NULL"; @@ -4747,7 +4953,7 @@ public void FilterOnVirtualTypeAttributeNotNull() [TestMethod] public void SubqueriesInValueList() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT a FROM (VALUES ('a'), ((SELECT TOP 1 firstname FROM contact)), ('b'), (1)) AS MyTable (a)"; @@ -4776,7 +4982,7 @@ public void SubqueriesInValueList() [TestMethod] public void FoldFilterOnIdentity() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT name FROM account WHERE accountid = @@IDENTITY"; @@ -4801,7 +5007,7 @@ public void FoldFilterOnIdentity() [TestMethod] public void FoldPrimaryIdInQuery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT name FROM account WHERE accountid IN (SELECT accountid FROM account INNER JOIN contact ON account.primarycontactid = contact.contactid WHERE name = 'Data8')"; @@ -4827,7 +5033,7 @@ public void FoldPrimaryIdInQuery() [TestMethod] public void FoldPrimaryIdInQueryWithTop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"DELETE FROM account WHERE accountid IN (SELECT TOP 10 accountid FROM account ORDER BY createdon DESC)"; @@ -4850,7 +5056,7 @@ public void FoldPrimaryIdInQueryWithTop() [TestMethod] public void InsertParameters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"DECLARE @name varchar(100) = 'test'; INSERT INTO account (name) VALUES (@name)"; @@ -4868,7 +5074,7 @@ public void InsertParameters() [TestMethod] public void NotExistsParameters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"DECLARE @firstname AS VARCHAR (100) = 'Mark', @lastname AS VARCHAR (100) = 'Carrington'; @@ -4909,7 +5115,7 @@ INSERT INTO contact (firstname, lastname) [TestMethod] public void UpdateParameters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"declare @name varchar(100) = 'Data8', @employees int = 10 UPDATE account SET employees = @employees WHERE name = @name"; @@ -4940,7 +5146,7 @@ public void UpdateParameters() [TestMethod] public void CountUsesAggregateByDefault() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT count(*) FROM account"; @@ -4964,7 +5170,7 @@ public void CountUsesAggregateByDefault() [TestMethod] public void CountUsesRetrieveTotalRecordCountWithHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT count(*) FROM account OPTION (USE HINT ('RETRIEVE_TOTAL_RECORD_COUNT'))"; @@ -4983,7 +5189,7 @@ public void CountUsesRetrieveTotalRecordCountWithHint() [TestMethod] public void MaxDOPUsesDefault() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"UPDATE account SET name = 'test'"; @@ -4998,7 +5204,7 @@ public void MaxDOPUsesDefault() [TestMethod] public void MaxDOPUsesHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"UPDATE account SET name = 'test' OPTION (MAXDOP 7)"; @@ -5010,10 +5216,26 @@ public void MaxDOPUsesHint() Assert.AreEqual(7, update.MaxDOP); } + [TestMethod] + public void MaxDOPUsesHintInsideIfBlock() + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @"IF (1 = 1) BEGIN UPDATE account SET name = 'test' OPTION (MAXDOP 7) END"; + + var plans = planBuilder.Build(query, null, out _); + + Assert.AreEqual(1, plans.Length); + + var cond = AssertNode(plans[0]); + var update = AssertNode(cond.TrueStatements[0]); + Assert.AreEqual(7, update.MaxDOP); + } + [TestMethod] public void SubqueryUsesSpoolByDefault() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT accountid, (SELECT TOP 1 fullname FROM contact) FROM account"; @@ -5031,7 +5253,7 @@ public void SubqueryUsesSpoolByDefault() [TestMethod] public void SubqueryDoesntUseSpoolWithHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT accountid, (SELECT TOP 1 fullname FROM contact) FROM account OPTION (NO_PERFORMANCE_SPOOL)"; @@ -5048,7 +5270,7 @@ public void SubqueryDoesntUseSpoolWithHint() [TestMethod] public void BypassPluginExecutionUsesDefault() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"UPDATE account SET name = 'test'"; @@ -5063,7 +5285,7 @@ public void BypassPluginExecutionUsesDefault() [TestMethod] public void BypassPluginExecutionUsesHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"UPDATE account SET name = 'test' OPTION (USE HINT ('BYPASS_CUSTOM_PLUGIN_EXECUTION'))"; @@ -5078,7 +5300,7 @@ public void BypassPluginExecutionUsesHint() [TestMethod] public void PageSizeUsesHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"SELECT name FROM account OPTION (USE HINT ('FETCHXML_PAGE_SIZE_100'))"; @@ -5100,7 +5322,7 @@ public void PageSizeUsesHint() [TestMethod] public void DistinctOrderByOptionSet() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT new_optionsetvalue FROM new_customentity ORDER BY new_optionsetvalue"; @@ -5108,13 +5330,13 @@ public void DistinctOrderByOptionSet() Assert.AreEqual(1, plans.Length); var select = AssertNode(plans[0]); - var sort = AssertNode(select.Source); - var fetch = AssertNode(sort.Source); + var fetch = AssertNode(select.Source); AssertFetchXml(fetch, @" - + + "); } @@ -5122,7 +5344,7 @@ public void DistinctOrderByOptionSet() [TestMethod] public void DistinctVirtualAttribute() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT DISTINCT new_optionsetvaluename FROM new_customentity"; @@ -5147,7 +5369,7 @@ public void DistinctVirtualAttribute() [TestMethod] public void TopAliasStar() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT TOP 10 A.* FROM account A"; @@ -5168,7 +5390,7 @@ public void TopAliasStar() [TestMethod] public void OrderByStar() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account ORDER BY primarycontactid"; @@ -5190,7 +5412,7 @@ public void OrderByStar() [TestMethod] public void UpdateColumnInWhereClause() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "UPDATE account SET name = '1' WHERE name <> '1'"; @@ -5220,7 +5442,7 @@ public void UpdateColumnInWhereClause() [TestMethod] public void NestedOrFilters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account WHERE name = '1' OR name = '2' OR name = '3' OR name = '4'"; @@ -5248,7 +5470,7 @@ public void NestedOrFilters() [TestMethod] public void UnknownHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account OPTION(USE HINT('invalid'))"; @@ -5258,7 +5480,7 @@ public void UnknownHint() [TestMethod] public void MultipleTablesJoinFromWhereClause() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT firstname FROM account, contact WHERE accountid = parentcustomerid AND lastname = 'Carrington' AND name = 'Data8'"; @@ -5287,7 +5509,7 @@ public void MultipleTablesJoinFromWhereClause() [TestMethod] public void MultipleTablesJoinFromWhereClauseReversed() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT firstname FROM account, contact WHERE lastname = 'Carrington' AND name = 'Data8' AND parentcustomerid = accountid"; @@ -5316,7 +5538,7 @@ public void MultipleTablesJoinFromWhereClauseReversed() [TestMethod] public void MultipleTablesJoinFromWhereClause3() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT firstname FROM account, contact, systemuser WHERE accountid = parentcustomerid AND lastname = 'Carrington' AND name = 'Data8' AND account.ownerid = systemuserid"; @@ -5346,7 +5568,7 @@ public void MultipleTablesJoinFromWhereClause3() [TestMethod] public void NestedInSubqueries() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT firstname FROM contact WHERE parentcustomerid IN (SELECT accountid FROM account WHERE primarycontactid IN (SELECT contactid FROM contact WHERE lastname = 'Carrington'))"; @@ -5374,7 +5596,7 @@ public void NestedInSubqueries() [TestMethod] public void SpoolNestedLoop() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT account.name, contact.fullname FROM account INNER JOIN contact ON account.accountid = contact.parentcustomerid OR account.createdon < contact.createdon"; @@ -5409,7 +5631,7 @@ public void SpoolNestedLoop() [TestMethod] public void SelectFromTVF() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM SampleMessage('test')"; @@ -5428,7 +5650,7 @@ public void SelectFromTVF() [TestMethod] public void OuterApplyCorrelatedTVF() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT account.name, msg.OutputParam1 FROM account OUTER APPLY (SELECT * FROM SampleMessage(account.name)) AS msg WHERE account.name = 'Data8'"; @@ -5455,7 +5677,7 @@ public void OuterApplyCorrelatedTVF() [TestMethod] public void OuterApplyUncorrelatedTVF() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT account.name, msg.OutputParam1 FROM account OUTER APPLY (SELECT * FROM SampleMessage('test')) AS msg WHERE account.name = 'Data8'"; @@ -5482,7 +5704,7 @@ public void OuterApplyUncorrelatedTVF() [TestMethod] public void TVFScalarSubqueryParameter() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM SampleMessage((SELECT TOP 1 name FROM account))"; @@ -5523,7 +5745,7 @@ public void TVFScalarSubqueryParameter() [TestMethod] public void ExecuteSproc() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "EXEC SampleMessage 'test'"; @@ -5540,7 +5762,7 @@ public void ExecuteSproc() [TestMethod] public void ExecuteSprocNamedParameters() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"DECLARE @i int EXEC SampleMessage @StringParam = 'test', @OutputParam2 = @i OUTPUT @@ -5558,30 +5780,33 @@ public void ExecuteSprocNamedParameters() [TestMethod] public void FoldMultipleJoinConditionsWithKnownValue() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + using (_localDataSource.SetColumnComparison(false)) + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); - var query = @"SELECT a.name, c.fullname FROM account a INNER JOIN contact c ON a.accountid = c.parentcustomerid AND a.name = c.fullname WHERE a.name = 'Data8'"; + var query = @"SELECT a.name, c.fullname FROM account a INNER JOIN contact c ON a.accountid = c.parentcustomerid AND a.name = c.fullname WHERE a.name = 'Data8'"; - var plans = planBuilder.Build(query, null, out _); + var plans = planBuilder.Build(query, null, out _); - Assert.AreEqual(1, plans.Length); - var select = AssertNode(plans[0]); - var fetch = AssertNode(select.Source); - AssertFetchXml(fetch, @" - - - - - + Assert.AreEqual(1, plans.Length); + var select = AssertNode(plans[0]); + var fetch = AssertNode(select.Source); + AssertFetchXml(fetch, @" + + + + + + + + + - + - - - - - - "); + + "); + } } [TestMethod] @@ -6158,7 +6383,7 @@ public void FoldSortOrderToInnerJoinLeftInput() [TestMethod] public void UpdateFromSubquery() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "UPDATE account SET name = 'foo' FROM account INNER JOIN (SELECT name, MIN(createdon) FROM account GROUP BY name HAVING COUNT(*) > 1) AS dupes ON account.name = dupes.name"; @@ -6182,7 +6407,7 @@ public void UpdateFromSubquery() [TestMethod] public void MinPrimaryKey() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT MIN(accountid) FROM account"; @@ -6204,7 +6429,7 @@ public void MinPrimaryKey() [TestMethod] public void MinPicklist() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT MIN(new_optionsetvalue) FROM new_customentity"; @@ -6227,7 +6452,7 @@ public void MinPicklist() [ExpectedException(typeof(NotSupportedQueryFragmentException))] public void AvgGuidIsNotSupported() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT AVG(accountid) FROM account"; planBuilder.Build(query, null, out _); } @@ -6235,7 +6460,7 @@ public void AvgGuidIsNotSupported() [TestMethod] public void StringAggWithOrderAndNoGroups() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT STRING_AGG(name, ',') WITHIN GROUP (ORDER BY name DESC) FROM account"; @@ -6258,7 +6483,7 @@ public void StringAggWithOrderAndNoGroups() [TestMethod] public void StringAggWithOrderAndScalarGroups() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT STRING_AGG(name, ',') WITHIN GROUP (ORDER BY name DESC) FROM account GROUP BY employees"; @@ -6283,7 +6508,7 @@ public void StringAggWithOrderAndScalarGroups() [TestMethod] public void StringAggWithOrderAndNonScalarGroups() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT STRING_AGG(name, ',') WITHIN GROUP (ORDER BY name DESC) FROM account GROUP BY name + 'x'"; @@ -6307,7 +6532,7 @@ public void StringAggWithOrderAndNonScalarGroups() [TestMethod] public void NestedExistsAndIn() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "IF NOT EXISTS(SELECT * FROM account WHERE primarycontactid IN (SELECT contactid FROM contact WHERE firstname = 'Mark')) SELECT 1"; @@ -6318,7 +6543,7 @@ public void NestedExistsAndIn() [TestMethod] public void HashJoinUsedForDifferentDataTypes() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account WHERE EXISTS(SELECT * FROM contact WHERE account.name = contact.createdon)"; @@ -6349,7 +6574,7 @@ public void DoNotFoldFilterOnParameterToIndexSpool() { // Subquery on right side of nested loop will use an index spool to reduce number of FetchXML requests. Do not use this logic if the // filter variable is an external parameter or the FetchXML is on the left side of the loop - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "SELECT * FROM account WHERE name = @name and primarycontactid = (SELECT contactid FROM contact WHERE firstname = 'Mark')"; @@ -6400,7 +6625,7 @@ public void DoNotFoldFilterOnParameterToIndexSpool() [TestMethod] public void DoNotFoldJoinsOnReusedAliases() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT s.systemuserid, @@ -6656,9 +6881,7 @@ public void DoNotUseCustomPagingForInJoin() { // https://github.com/MarkMpn/Sql4Cds/issues/366 - _supportedJoins.Add(JoinOperator.Any); - - try + using (_dataSource.EnableJoinOperator(JoinOperator.In)) { var planBuilder = new ExecutionPlanBuilder(_dataSources.Values, new OptionsWrapper(this) { PrimaryDataSource = "uat" }); @@ -6675,7 +6898,7 @@ WHERE contactid IN (SELECT DISTINCT primarycontactid FROM account WHERE name = - + @@ -6691,10 +6914,6 @@ WHERE contactid IN (SELECT DISTINCT primarycontactid FROM account WHERE name = "); } - finally - { - _supportedJoins.Remove(JoinOperator.Any); - } } [TestMethod] @@ -6703,7 +6922,7 @@ public void FoldFilterToCorrectTableAlias() // The same table alias can be used in the main query and in a query-derived table. Ensure filters are // folded to the correct one. - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT * @@ -6747,7 +6966,7 @@ public void FoldFilterToCorrectTableAlias() [TestMethod] public void IgnoreDupKeyHint() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @"INSERT INTO account (accountid, name) VALUES ('{CD503427-E785-40D8-AD0E-FBDF4918D298}', 'Data8') OPTION (USE HINT ('IGNORE_DUP_KEY'))"; @@ -6762,7 +6981,7 @@ public void IgnoreDupKeyHint() [TestMethod] public void GroupByWithoutAggregateUsesDistinct() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT @@ -6789,7 +7008,7 @@ public void GroupByWithoutAggregateUsesDistinct() [TestMethod] public void FilterOnCrossApply() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" select name, n from account @@ -6825,7 +7044,7 @@ cross apply (select name + '' as n) x [TestMethod] public void GotoCantMoveIntoTryBlock() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" GOTO label1 @@ -6852,7 +7071,7 @@ BEGIN CATCH [DataRow(3)] public void UpdateTop(int top) { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = $@" UPDATE account @@ -6887,7 +7106,7 @@ AND employees > 0 [TestMethod] public void RethrowMustBeWithinCatchBlock() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = "THROW;"; @@ -6905,7 +7124,7 @@ public void RethrowMustBeWithinCatchBlock() [TestMethod] public void MistypedJoinCriteriaGeneratesWarning() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" SELECT a.name, c.fullname @@ -6945,7 +7164,7 @@ public void MistypedJoinCriteriaGeneratesWarning() [TestMethod] public void AliasSameAsVirtualAttribute() { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var query = @" select a.name, c.fullname as primarycontactidname from account a @@ -6967,5 +7186,215 @@ 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, @" + + + + + + + "); + } + + [TestMethod] + public void ExistsOrInAndColumnComparisonOrderByEntityName() + { + using (_localDataSource.EnableJoinOperator(JoinOperator.Any)) + using (_localDataSource.SetOrderByEntityName(true)) + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @" +SELECT TOP 100 + account.name, + contact.fullname +FROM + account + INNER JOIN contact ON account.primarycontactid = contact.contactid +WHERE + ( + EXISTS (SELECT * FROM contact WHERE parentcustomerid = account.accountid AND firstname = 'Mark') + OR employees in (SELECT employees FROM account WHERE name = 'Data8') + ) + AND account.createdon = contact.createdon +ORDER BY + contact.fullname, account.name"; + + 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 ExistsOrInAndColumnComparisonOrderByEntityNameLegacy() + { + using (_localDataSource.SetColumnComparison(false)) + { + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + + var query = @" +SELECT TOP 100 + account.name, + contact.fullname +FROM + account + INNER JOIN contact ON account.primarycontactid = contact.contactid +WHERE + ( + EXISTS (SELECT * FROM contact WHERE parentcustomerid = account.accountid AND firstname = 'Mark') + OR employees in (SELECT employees FROM account WHERE name = 'Data8') + ) + AND account.createdon = contact.createdon +ORDER BY + contact.fullname, account.name"; + + var plans = planBuilder.Build(query, null, out _); + + Assert.AreEqual(1, plans.Length); + + var select = AssertNode(plans[0]); + var top = AssertNode(select.Source); + var sort = AssertNode(top.Source); + var filter = AssertNode(sort.Source); + var loop = AssertNode(filter.Source); + var merge = AssertNode(loop.LeftSource); + var mainFetch = AssertNode(merge.LeftSource); + var existsFetch = AssertNode(merge.RightSource); + var inTop = AssertNode(loop.RightSource); + var inIndexSpool = AssertNode(inTop.Source); + var inFetch = AssertNode(inIndexSpool.Source); + + AssertFetchXml(mainFetch, @" + + + + + + + + + + + + +"); + + AssertFetchXml(existsFetch, @" + + + + + + + + +"); + + AssertFetchXml(inFetch, @" + + + + + + + + + +"); + } + } } } diff --git a/MarkMpn.Sql4Cds.Engine.Tests/ExpressionTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/ExpressionTests.cs index 4747b67b..ff6c0273 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/ExpressionTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/ExpressionTests.cs @@ -23,7 +23,7 @@ public void StringLiteral() var schema = new NodeSchema(new Dictionary(), new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var actual = func(new ExpressionExecutionContext(compilationContext)); @@ -40,7 +40,7 @@ public void IntegerLiteral() var schema = new NodeSchema(new Dictionary(), new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var actual = func(new ExpressionExecutionContext(compilationContext)); @@ -62,7 +62,7 @@ public void StringConcat() var schema = new NodeSchema(new Dictionary(), new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var actual = func(new ExpressionExecutionContext(compilationContext)); @@ -83,7 +83,7 @@ public void IntegerAddition() var schema = new NodeSchema(new Dictionary(), new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var actual = func(new ExpressionExecutionContext(compilationContext)); @@ -118,7 +118,7 @@ public void SimpleCaseExpression() }, new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var record = new Entity @@ -158,7 +158,7 @@ public void FormatDateTime() }, new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var record = new Entity @@ -186,7 +186,7 @@ public void LikeWithEmbeddedReturns() }, new Dictionary>(), null, Array.Empty()); var parameterTypes = new Dictionary(); var options = new StubOptions(); - var compilationContext = new ExpressionCompilationContext(_localDataSource, options, parameterTypes, schema, null); + var compilationContext = new ExpressionCompilationContext(_localDataSources, options, parameterTypes, schema, null); var func = expr.Compile(compilationContext); var record = new Entity diff --git a/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs new file mode 100644 index 00000000..52395a19 --- /dev/null +++ b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmDataSource.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Xrm.Sdk.Query; + +namespace MarkMpn.Sql4Cds.Engine.Tests +{ + public class FakeXrmDataSource : DataSource + { + class Reset : IDisposable + { + private readonly FakeXrmDataSource _target; + private readonly Action _reset; + + public Reset(FakeXrmDataSource target, Action set, Action reset) + { + _target = target; + _reset = reset; + set(target); + } + + public void Dispose() + { + _reset(_target); + } + } + + private bool _columnComparisonAvailable = true; + private bool _orderByEntityNameAvailable = false; + private List _joinOperators; + + public FakeXrmDataSource() + { + _joinOperators = new List { JoinOperator.Inner, JoinOperator.LeftOuter }; + } + + public override bool ColumnComparisonAvailable => _columnComparisonAvailable; + + public override bool OrderByEntityNameAvailable => _orderByEntityNameAvailable; + + public override List JoinOperatorsAvailable => _joinOperators; + + public IDisposable SetColumnComparison(bool enable) + { + var original = _columnComparisonAvailable; + return new Reset(this, x => x._columnComparisonAvailable = enable, x => x._columnComparisonAvailable = original); + } + + public IDisposable SetOrderByEntityName(bool enable) + { + var original = _orderByEntityNameAvailable; + return new Reset(this, x => x._orderByEntityNameAvailable = enable, x => x._orderByEntityNameAvailable = original); + } + + public IDisposable EnableJoinOperator(JoinOperator op) + { + var add = !JoinOperatorsAvailable.Contains(op); + return new Reset(this, + x => + { + if (add) + x.JoinOperatorsAvailable.Add(op); + }, + x => + { + if (add) + x.JoinOperatorsAvailable.Remove(op); + }); + } + } +} diff --git a/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmEasyTestsBase.cs b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmEasyTestsBase.cs index 68f7c6b1..26b2dde8 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmEasyTestsBase.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/FakeXrmEasyTestsBase.cs @@ -20,15 +20,16 @@ public class FakeXrmEasyTestsBase { protected readonly IOrganizationService _service; protected readonly XrmFakedContext _context; - protected readonly DataSource _dataSource; + protected readonly FakeXrmDataSource _dataSource; protected readonly IOrganizationService _service2; protected readonly XrmFakedContext _context2; - protected readonly DataSource _dataSource2; + protected readonly FakeXrmDataSource _dataSource2; protected readonly IOrganizationService _service3; protected readonly XrmFakedContext _context3; - protected readonly DataSource _dataSource3; + protected readonly FakeXrmDataSource _dataSource3; protected readonly IDictionary _dataSources; - protected readonly IDictionary _localDataSource; + protected readonly FakeXrmDataSource _localDataSource; + protected readonly IDictionary _localDataSources; static FakeXrmEasyTestsBase() { @@ -73,7 +74,7 @@ public FakeXrmEasyTestsBase() _context.AddGenericFakeMessageExecutor(SetStateMessageExecutor.MessageName, new SetStateMessageExecutor()); _service = _context.GetOrganizationService(); - _dataSource = new DataSource { Name = "uat", Connection = _service, Metadata = new AttributeMetadataCache(_service), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = Collation.USEnglish }; + _dataSource = new FakeXrmDataSource { Name = "uat", Connection = _service, Metadata = new AttributeMetadataCache(_service), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = Collation.USEnglish }; _context.AddFakeMessageExecutor(new RetrieveMetadataChangesHandler(_dataSource.Metadata)); _context2 = new XrmFakedContext(); @@ -85,7 +86,7 @@ public FakeXrmEasyTestsBase() _context2.AddGenericFakeMessageExecutor(SetStateMessageExecutor.MessageName, new SetStateMessageExecutor()); _service2 = _context2.GetOrganizationService(); - _dataSource2 = new DataSource { Name = "prod", Connection = _service2, Metadata = new AttributeMetadataCache(_service2), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = Collation.USEnglish }; + _dataSource2 = new FakeXrmDataSource { Name = "prod", Connection = _service2, Metadata = new AttributeMetadataCache(_service2), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = Collation.USEnglish }; _context2.AddFakeMessageExecutor(new RetrieveMetadataChangesHandler(_dataSource2.Metadata)); _context3 = new XrmFakedContext(); @@ -98,13 +99,15 @@ public FakeXrmEasyTestsBase() _service3 = _context3.GetOrganizationService(); Collation.TryParse("French_CI_AI", out var frenchCIAI); - _dataSource3 = new DataSource { Name = "french", Connection = _service3, Metadata = new AttributeMetadataCache(_service3), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = frenchCIAI }; + _dataSource3 = new FakeXrmDataSource { Name = "french", Connection = _service3, Metadata = new AttributeMetadataCache(_service3), TableSizeCache = new StubTableSizeCache(), MessageCache = new StubMessageCache(), DefaultCollation = frenchCIAI }; _context3.AddFakeMessageExecutor(new RetrieveMetadataChangesHandler(_dataSource3.Metadata)); - _dataSources = new[] { _dataSource, _dataSource2, _dataSource3 }.ToDictionary(ds => ds.Name); - _localDataSource = new Dictionary + _dataSources = new[] { _dataSource, _dataSource2, _dataSource3 }.ToDictionary(ds => ds.Name, ds => (DataSource)ds); + + _localDataSource = new FakeXrmDataSource { Name = "local", Connection = _service, Metadata = _dataSource.Metadata, TableSizeCache = _dataSource.TableSizeCache, MessageCache = _dataSource.MessageCache, DefaultCollation = Collation.USEnglish }; + _localDataSources = new Dictionary { - ["local"] = new DataSource { Name = "local", Connection = _service, Metadata = _dataSource.Metadata, TableSizeCache = _dataSource.TableSizeCache, MessageCache = _dataSource.MessageCache, DefaultCollation = Collation.USEnglish } + ["local"] = _localDataSource }; SetPrimaryIdAttributes(_context); diff --git a/MarkMpn.Sql4Cds.Engine.Tests/JsonFunctionTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/JsonFunctionTests.cs index e1daff03..44a87a48 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/JsonFunctionTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/JsonFunctionTests.cs @@ -28,7 +28,7 @@ public class JsonFunctionTests : FakeXrmEasyTestsBase [DataRow("strict $.info.none", null, true)] public void JsonValue(string path, string expectedValue, bool expectedError) { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -71,7 +71,7 @@ DECLARE @jsonInfo NVARCHAR(MAX) [TestMethod] public void JsonValueNull() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT JSON_VALUE('{ \"changedAttributes\": [ { \"logicalName\": \"column1\", \"oldValue\": null, \"newValue\": \"\" } ] }', '$.changedAttributes[0].oldValue')"; @@ -104,7 +104,7 @@ public void JsonValueNull() [DataRow("strict $.info.none", null, false, true)] public void JsonQuery(string path, string expectedValue, bool expectedFullValue, bool expectedError) { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT JSON_QUERY(@json, @path)"; @@ -150,7 +150,7 @@ public void JsonQuery(string path, string expectedValue, bool expectedFullValue, [TestMethod] public void IsJsonTrue() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT ISJSON('true', VALUE)"; @@ -161,7 +161,7 @@ public void IsJsonTrue() [TestMethod] public void IsJsonUnquotedString() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT ISJSON('test string', VALUE)"; @@ -172,7 +172,7 @@ public void IsJsonUnquotedString() [TestMethod] public void IsJsonQuotedString() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT ISJSON('\"test string\"', SCALAR)"; diff --git a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj index d9a2ecc1..2b7dcf86 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj +++ b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj @@ -81,6 +81,7 @@ + @@ -120,6 +121,9 @@ + + 2.1.28 + 1.58.1 diff --git a/MarkMpn.Sql4Cds.Engine.Tests/OpenJsonTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/OpenJsonTests.cs index 99bd3681..f5d13bf7 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/OpenJsonTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/OpenJsonTests.cs @@ -13,7 +13,7 @@ public class OpenJsonTests : FakeXrmEasyTestsBase [TestMethod] public void OpenJsonDefaultSchema() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -58,7 +58,7 @@ DECLARE @json NVARCHAR(MAX) [TestMethod] public void OpenJsonDefaultSchemaWithPath() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -106,7 +106,7 @@ DECLARE @json NVARCHAR(4000) = N'{ [TestMethod] public void OpenJsonDefaultSchemaDataTypes() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -177,7 +177,7 @@ DECLARE @json NVARCHAR(2048) = N'{ [TestMethod] public void OpenJsonExplicitSchema() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -252,7 +252,7 @@ [Order] NVARCHAR(MAX) AS JSON [TestMethod] public void MergeJson() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -298,7 +298,7 @@ FROM OPENJSON(@json2) [TestMethod] public void NestedJson() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" @@ -388,7 +388,7 @@ FROM OPENJSON ( @JSON ) AS root [TestMethod] public void RecursiveCTEJson() { - using (var con = new Sql4CdsConnection(_localDataSource)) + using (var con = new Sql4CdsConnection(_localDataSources)) using (var cmd = con.CreateCommand()) { cmd.CommandText = @" diff --git a/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs index 97482a8f..ac29f142 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/OptionsWrapper.cs @@ -24,9 +24,7 @@ public OptionsWrapper(IQueryExecutionOptions options) BatchSize = options.BatchSize; UseTDSEndpoint = options.UseTDSEndpoint; MaxDegreeOfParallelism = options.MaxDegreeOfParallelism; - ColumnComparisonAvailable = options.ColumnComparisonAvailable; UseLocalTimeZone = options.UseLocalTimeZone; - JoinOperatorsAvailable = new List(options.JoinOperatorsAvailable); BypassCustomPlugins = options.BypassCustomPlugins; PrimaryDataSource = options.PrimaryDataSource; UserId = options.UserId; @@ -50,12 +48,8 @@ public OptionsWrapper(IQueryExecutionOptions options) public int MaxDegreeOfParallelism { get; set; } - public bool ColumnComparisonAvailable { get; set; } - public bool UseLocalTimeZone { get; set; } - public List JoinOperatorsAvailable { get; set; } - public bool BypassCustomPlugins { get; set; } public string PrimaryDataSource { get; set; } diff --git a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs index 4565cb28..24ab64aa 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/Sql2FetchXmlTests.cs @@ -42,12 +42,8 @@ public class Sql2FetchXmlTests : FakeXrmEasyTestsBase, IQueryExecutionOptions int IQueryExecutionOptions.MaxDegreeOfParallelism => 10; - bool IQueryExecutionOptions.ColumnComparisonAvailable => true; - bool IQueryExecutionOptions.UseLocalTimeZone => false; - List IQueryExecutionOptions.JoinOperatorsAvailable => new List { JoinOperator.Inner, JoinOperator.LeftOuter }; - bool IQueryExecutionOptions.BypassCustomPlugins => false; string IQueryExecutionOptions.PrimaryDataSource => "local"; @@ -63,7 +59,7 @@ public void SimpleSelect() { var query = "SELECT accountid, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -81,7 +77,7 @@ public void SelectSameFieldMultipleTimes() { var query = "SELECT accountid, name, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -106,7 +102,7 @@ public void SelectStar() { var query = "SELECT * FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -139,7 +135,7 @@ public void SelectStarAndField() { var query = "SELECT *, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -173,7 +169,7 @@ public void SimpleFilter() { var query = "SELECT accountid, name FROM account WHERE name = 'test'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -194,7 +190,7 @@ public void BetweenFilter() { var query = "SELECT accountid, name FROM account WHERE employees BETWEEN 1 AND 10 AND turnover NOT BETWEEN 2 AND 20"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -220,7 +216,7 @@ public void FetchFilter() { var query = "SELECT contactid, firstname FROM contact WHERE createdon = lastxdays(7)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -241,7 +237,7 @@ public void NestedFilters() { var query = "SELECT accountid, name FROM account WHERE name = 'test' OR (accountid is not null and name like 'foo%')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -266,7 +262,7 @@ public void Sorts() { var query = "SELECT accountid, name FROM account ORDER BY name DESC, accountid"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -286,7 +282,7 @@ public void SortByColumnIndex() { var query = "SELECT accountid, name FROM account ORDER BY 2 DESC, 1"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -306,7 +302,7 @@ public void SortByAliasedColumn() { var query = "SELECT accountid, name as accountname FROM account ORDER BY name"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -325,7 +321,7 @@ public void Top() { var query = "SELECT TOP 10 accountid, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -343,7 +339,7 @@ public void TopBrackets() { var query = "SELECT TOP (10) accountid, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -361,7 +357,7 @@ public void Top10KUsesExtension() { var query = "SELECT TOP 10000 accountid, name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -382,7 +378,7 @@ public void NoLock() { var query = "SELECT accountid, name FROM account (NOLOCK)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -400,7 +396,7 @@ public void Distinct() { var query = "SELECT DISTINCT name FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -418,7 +414,7 @@ public void Offset() { var query = "SELECT accountid, name FROM account ORDER BY name OFFSET 100 ROWS FETCH NEXT 50 ROWS ONLY"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -437,7 +433,7 @@ public void SimpleJoin() { var query = "SELECT accountid, name FROM account INNER JOIN contact ON primarycontactid = contactid"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -457,7 +453,7 @@ public void SelfReferentialJoin() { var query = "SELECT contact.contactid, contact.firstname, manager.firstname FROM contact LEFT OUTER JOIN contact AS manager ON contact.parentcustomerid = manager.contactid"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -478,7 +474,7 @@ public void AdditionalJoinCriteria() { var query = "SELECT accountid, name FROM account INNER JOIN contact ON accountid = parentcustomerid AND (firstname = 'Mark' OR lastname = 'Carrington')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -502,7 +498,7 @@ public void InvalidAdditionalJoinCriteria() { var query = "SELECT accountid, name FROM account INNER JOIN contact ON accountid = parentcustomerid OR (firstname = 'Mark' AND lastname = 'Carrington')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); Assert.IsNotInstanceOfType(((SelectNode)queries[0]).Source, typeof(FetchXmlScan)); @@ -513,7 +509,7 @@ public void SortOnLinkEntity() { var query = "SELECT TOP 100 accountid, name FROM account INNER JOIN contact ON primarycontactid = contactid ORDER BY name, firstname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -535,7 +531,7 @@ public void InvalidSortOnLinkEntity() { var query = "SELECT TOP 100 accountid, name FROM account INNER JOIN contact ON accountid = parentcustomerid ORDER BY name, firstname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -560,7 +556,7 @@ public void SimpleAggregate() { var query = "SELECT count(*), count(name), count(DISTINCT name), max(name), min(name), avg(employees) FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -582,7 +578,7 @@ public void GroupBy() { var query = "SELECT name, count(*) FROM account GROUP BY name"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -601,7 +597,7 @@ public void GroupBySorting() { var query = "SELECT name, count(*) FROM account GROUP BY name ORDER BY name, count(*)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -621,7 +617,7 @@ public void GroupBySortingOnLinkEntity() { var query = "SELECT name, firstname, count(*) FROM account INNER JOIN contact ON parentcustomerid = account.accountid GROUP BY name, firstname ORDER BY firstname, name"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -644,7 +640,7 @@ public void GroupBySortingOnAliasedAggregate() { var query = "SELECT name, firstname, count(*) as count FROM account INNER JOIN contact ON parentcustomerid = account.accountid GROUP BY name, firstname ORDER BY count"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -666,7 +662,7 @@ public void UpdateFieldToValue() { var query = "UPDATE contact SET firstname = 'Mark'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -683,7 +679,7 @@ public void SelectArithmetic() { var query = "SELECT employees + 1 AS a, employees * 2 AS b, turnover / 3 AS c, turnover - 4 AS d, turnover / employees AS e FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -721,12 +717,14 @@ public void SelectArithmetic() [TestMethod] public void WhereComparingTwoFields() { - var query = "SELECT contactid FROM contact WHERE firstname = lastname"; + using (_localDataSource.SetColumnComparison(false)) + { + var query = "SELECT contactid FROM contact WHERE firstname = lastname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, new OptionsWrapper(this) { ColumnComparisonAvailable = false }); - var queries = planBuilder.Build(query, null, out _); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + var queries = planBuilder.Build(query, null, out _); - AssertFetchXml(queries, @" + AssertFetchXml(queries, @" @@ -736,30 +734,31 @@ public void WhereComparingTwoFields() "); - var guid1 = Guid.NewGuid(); - var guid2 = Guid.NewGuid(); - _context.Data["contact"] = new Dictionary - { - [guid1] = new Entity("contact", guid1) + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + _context.Data["contact"] = new Dictionary { - ["contactid"] = guid1, - ["firstname"] = "Mark", - ["lastname"] = "Carrington" - }, - [guid2] = new Entity("contact", guid2) - { - ["contactid"] = guid2, - ["firstname"] = "Mark", - ["lastname"] = "Mark" - } - }; + [guid1] = new Entity("contact", guid1) + { + ["contactid"] = guid1, + ["firstname"] = "Mark", + ["lastname"] = "Carrington" + }, + [guid2] = new Entity("contact", guid2) + { + ["contactid"] = guid2, + ["firstname"] = "Mark", + ["lastname"] = "Mark" + } + }; - var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); - var dataTable = new DataTable(); - dataTable.Load(dataReader); + var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); + var dataTable = new DataTable(); + dataTable.Load(dataReader); - Assert.AreEqual(1, dataTable.Rows.Count); - Assert.AreEqual(guid2, ((SqlEntityReference)dataTable.Rows[0]["contactid"]).Id); + Assert.AreEqual(1, dataTable.Rows.Count); + Assert.AreEqual(guid2, ((SqlEntityReference)dataTable.Rows[0]["contactid"]).Id); + } } [TestMethod] @@ -767,7 +766,7 @@ public void WhereComparingExpression() { var query = "SELECT contactid FROM contact WHERE lastname = firstname + 'rington'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -811,7 +810,7 @@ public void BackToFrontLikeExpression() { var query = "SELECT contactid FROM contact WHERE 'Mark' like firstname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -852,7 +851,7 @@ public void UpdateFieldToField() { var query = "UPDATE contact SET firstname = lastname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -884,7 +883,7 @@ public void UpdateFieldToExpression() { var query = "UPDATE contact SET firstname = 'Hello ' + lastname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -916,7 +915,7 @@ public void UpdateReplace() { var query = "UPDATE contact SET firstname = REPLACE(firstname, 'Dataflex Pro', 'CDS') WHERE lastname = 'Carrington'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -952,7 +951,7 @@ public void StringFunctions() { var query = "SELECT trim(firstname) as trim, ltrim(firstname) as ltrim, rtrim(firstname) as rtrim, substring(firstname, 2, 3) as substring23, len(firstname) as len FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -992,7 +991,7 @@ public void SelectExpression() { var query = "SELECT firstname, 'Hello ' + firstname AS greeting FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1042,7 +1041,7 @@ public void SelectExpressionNullValues() { var query = "SELECT firstname, 'Hello ' + firstname AS greeting, case when createdon > '2020-01-01' then 'new' else 'old' end AS age FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1078,7 +1077,7 @@ public void OrderByExpression() { var query = "SELECT firstname, lastname FROM contact ORDER BY lastname + ', ' + firstname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1122,7 +1121,7 @@ public void OrderByAliasedField() { var query = "SELECT firstname, lastname AS surname FROM contact ORDER BY surname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1167,7 +1166,7 @@ public void OrderByCalculatedField() { var query = "SELECT firstname, lastname, lastname + ', ' + firstname AS fullname FROM contact ORDER BY fullname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1211,7 +1210,7 @@ public void OrderByCalculatedFieldByIndex() { var query = "SELECT firstname, lastname, lastname + ', ' + firstname AS fullname FROM contact ORDER BY 3"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1255,7 +1254,7 @@ public void DateCalculations() { var query = "SELECT contactid, DATEADD(day, 1, createdon) AS nextday, DATEPART(minute, createdon) AS minute FROM contact WHERE DATEDIFF(hour, '2020-01-01', createdon) < 1"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1296,12 +1295,14 @@ public void DateCalculations() [TestMethod] public void TopAppliedAfterCustomFilter() { - var query = "SELECT TOP 10 contactid FROM contact WHERE firstname = lastname"; + using (_localDataSource.SetColumnComparison(false)) + { + var query = "SELECT TOP 10 contactid FROM contact WHERE firstname = lastname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, new OptionsWrapper(this) { ColumnComparisonAvailable = false }); - var queries = planBuilder.Build(query, null, out _); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + var queries = planBuilder.Build(query, null, out _); - AssertFetchXml(queries, @" + AssertFetchXml(queries, @" @@ -1311,8 +1312,9 @@ public void TopAppliedAfterCustomFilter() "); - Assert.IsInstanceOfType(((SelectNode)queries[0]).Source, typeof(TopNode)); - Assert.IsInstanceOfType(((SelectNode)queries[0]).Source.GetSources().Single(), typeof(FilterNode)); + Assert.IsInstanceOfType(((SelectNode)queries[0]).Source, typeof(TopNode)); + Assert.IsInstanceOfType(((SelectNode)queries[0]).Source.GetSources().Single(), typeof(FilterNode)); + } } [TestMethod] @@ -1320,7 +1322,7 @@ public void CustomFilterAggregateHavingProjectionSortAndTop() { var query = "SELECT TOP 10 lastname, SUM(CASE WHEN firstname = 'Mark' THEN 1 ELSE 0 END) as nummarks, LEFT(lastname, 1) AS lastinitial FROM contact WHERE DATEDIFF(day, '2020-01-01', createdon) > 10 GROUP BY lastname HAVING count(*) > 1 ORDER BY 2 DESC"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1388,7 +1390,7 @@ public void FilterCaseInsensitive() { var query = "SELECT contactid FROM contact WHERE DATEDIFF(day, '2020-01-01', createdon) < 10 OR lastname = 'Carrington' ORDER BY createdon"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1446,7 +1448,7 @@ public void GroupCaseInsensitive() { var query = "SELECT lastname, count(*) FROM contact WHERE DATEDIFF(day, '2020-01-01', createdon) > 10 GROUP BY lastname ORDER BY 2 DESC"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1503,7 +1505,7 @@ public void AggregateExpressionsWithoutGrouping() { var query = "SELECT count(DISTINCT firstname + ' ' + lastname) AS distinctnames FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1555,7 +1557,7 @@ public void AggregateQueryProducesAlternative() { var query = "SELECT name, count(*) FROM account GROUP BY name ORDER BY 2 DESC"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var select = (SelectNode)queries[0]; @@ -1609,54 +1611,57 @@ public void AggregateQueryProducesAlternative() [TestMethod] public void GuidEntityReferenceInequality() { - var query = "SELECT a.name FROM account a INNER JOIN contact c ON a.primarycontactid = c.contactid WHERE (c.parentcustomerid is null or a.accountid <> c.parentcustomerid)"; + using (_localDataSource.SetColumnComparison(false)) + { + var query = "SELECT a.name FROM account a INNER JOIN contact c ON a.primarycontactid = c.contactid WHERE (c.parentcustomerid is null or a.accountid <> c.parentcustomerid)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); - var queries = planBuilder.Build(query, null, out _); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); + var queries = planBuilder.Build(query, null, out _); - var select = (SelectNode)queries[0]; + var select = (SelectNode)queries[0]; - var account1 = Guid.NewGuid(); - var account2 = Guid.NewGuid(); - var contact1 = Guid.NewGuid(); - var contact2 = Guid.NewGuid(); + var account1 = Guid.NewGuid(); + var account2 = Guid.NewGuid(); + var contact1 = Guid.NewGuid(); + var contact2 = Guid.NewGuid(); - _context.Data["account"] = new Dictionary - { - [account1] = new Entity("account", account1) - { - ["name"] = "Data8", - ["accountid"] = account1, - ["primarycontactid"] = new EntityReference("contact", contact1) - }, - [account2] = new Entity("account", account2) + _context.Data["account"] = new Dictionary { - ["name"] = "Microsoft", - ["accountid"] = account2, - ["primarycontactid"] = new EntityReference("contact", contact2) - } - }; - _context.Data["contact"] = new Dictionary - { - [contact1] = new Entity("contact", contact1) - { - ["parentcustomerid"] = new EntityReference("account", account2), - ["contactid"] = contact1 - }, - [contact2] = new Entity("contact", contact2) + [account1] = new Entity("account", account1) + { + ["name"] = "Data8", + ["accountid"] = account1, + ["primarycontactid"] = new EntityReference("contact", contact1) + }, + [account2] = new Entity("account", account2) + { + ["name"] = "Microsoft", + ["accountid"] = account2, + ["primarycontactid"] = new EntityReference("contact", contact2) + } + }; + _context.Data["contact"] = new Dictionary { - ["parentcustomerid"] = new EntityReference("account", account2), - ["contactid"] = contact2 - } - }; + [contact1] = new Entity("contact", contact1) + { + ["parentcustomerid"] = new EntityReference("account", account2), + ["contactid"] = contact1 + }, + [contact2] = new Entity("contact", contact2) + { + ["parentcustomerid"] = new EntityReference("account", account2), + ["contactid"] = contact2 + } + }; - var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); - var dataTable = new DataTable(); - dataTable.Load(dataReader); + var dataReader = select.Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); + var dataTable = new DataTable(); + dataTable.Load(dataReader); - Assert.AreEqual(1, dataTable.Rows.Count); + Assert.AreEqual(1, dataTable.Rows.Count); - Assert.AreEqual("Data8", dataTable.Rows[0]["name"]); + Assert.AreEqual("Data8", dataTable.Rows[0]["name"]); + } } [TestMethod] @@ -1664,7 +1669,7 @@ public void UpdateGuidToEntityReference() { var query = "UPDATE a SET primarycontactid = c.contactid FROM account AS a INNER JOIN contact AS c ON a.accountid = c.parentcustomerid"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var update = (UpdateNode)queries[0]; @@ -1712,25 +1717,26 @@ public void CompareDateFields() { var query = "DELETE c2 FROM contact c1 INNER JOIN contact c2 ON c1.parentcustomerid = c2.parentcustomerid AND c2.createdon > c1.createdon"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" - - + + + "); var delete = (DeleteNode)queries[0]; - Assert.IsNotInstanceOfType(delete.Source, typeof(FetchXmlScan)); + Assert.IsInstanceOfType(delete.Source, typeof(FetchXmlScan)); } [TestMethod] @@ -1738,7 +1744,7 @@ public void ColumnComparison() { var query = "SELECT firstname, lastname FROM contact WHERE firstname = lastname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -1761,7 +1767,7 @@ public void QuotedIdentifierError() try { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, new OptionsWrapper(this) { QuotedIdentifiers = true }); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, new OptionsWrapper(this) { QuotedIdentifiers = true }); var queries = planBuilder.Build(query, null, out _); Assert.Fail("Expected exception"); @@ -1777,7 +1783,7 @@ public void FilterExpressionConstantValueToFetchXml() { var query = "SELECT firstname, lastname FROM contact WHERE firstname = 'Ma' + 'rk'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1798,7 +1804,7 @@ public void Count1ConvertedToCountStar() { var query = "SELECT COUNT(1) FROM contact OPTION(USE HINT('RETRIEVE_TOTAL_RECORD_COUNT'))"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var selectNode = (SelectNode)queries[0]; @@ -1811,7 +1817,7 @@ public void CaseInsensitive() { var query = "Select Name From Account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1828,7 +1834,7 @@ public void ContainsValues1() { var query = "SELECT new_name FROM new_customentity WHERE CONTAINS(new_optionsetvaluecollection, '1')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1850,7 +1856,7 @@ public void ContainsValuesFunction1() { var query = "SELECT new_name FROM new_customentity WHERE new_optionsetvaluecollection = containvalues(1)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1872,7 +1878,7 @@ public void ContainsValues() { var query = "SELECT new_name FROM new_customentity WHERE CONTAINS(new_optionsetvaluecollection, '1 OR 2')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1895,7 +1901,7 @@ public void ContainsValuesFunction() { var query = "SELECT new_name FROM new_customentity WHERE new_optionsetvaluecollection = containvalues(1, 2)"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1918,7 +1924,7 @@ public void NotContainsValues() { var query = "SELECT new_name FROM new_customentity WHERE NOT CONTAINS(new_optionsetvaluecollection, '1 OR 2')"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, $@" @@ -1955,7 +1961,7 @@ public void ImplicitTypeConversion() { var query = "SELECT employees / 2.0 AS half FROM account"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var account1 = Guid.NewGuid(); @@ -1988,7 +1994,7 @@ public void ImplicitTypeConversionComparison() { var query = "SELECT accountid FROM account WHERE turnover / 2 > 10"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var account1 = Guid.NewGuid(); @@ -2020,7 +2026,7 @@ public void GlobalOptionSet() { var query = "SELECT displayname FROM metadata.globaloptionset WHERE name = 'test'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); Assert.IsInstanceOfType(queries.Single(), typeof(SelectNode)); @@ -2044,7 +2050,7 @@ public void EntityDetails() { var query = "SELECT logicalname FROM metadata.entity ORDER BY 1"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); Assert.IsInstanceOfType(queries.Single(), typeof(SelectNode)); @@ -2068,7 +2074,7 @@ public void AttributeDetails() { var query = "SELECT e.logicalname, a.logicalname FROM metadata.entity e INNER JOIN metadata.attribute a ON e.logicalname = a.entitylogicalname WHERE e.logicalname = 'new_customentity' ORDER BY 1, 2"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var dataReader = ((SelectNode)queries[0]).Execute(new NodeExecutionContext(GetDataSources(_context), this, new Dictionary(), new Dictionary(), null), CommandBehavior.Default); @@ -2093,7 +2099,7 @@ public void OptionSetNameSelect() { var query = "SELECT new_optionsetvalue, new_optionsetvaluename FROM new_customentity ORDER BY new_optionsetvaluename"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var record1 = Guid.NewGuid(); @@ -2144,7 +2150,7 @@ public void OptionSetNameFilter() { var query = "SELECT new_customentityid FROM new_customentity WHERE new_optionsetvaluename = 'Value1'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -2163,7 +2169,7 @@ public void EntityReferenceNameSelect() { var query = "SELECT primarycontactid, primarycontactidname FROM account ORDER BY primarycontactidname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -2184,7 +2190,7 @@ public void EntityReferenceNameFilter() { var query = "SELECT accountid FROM account WHERE primarycontactidname = 'Mark Carrington'"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -2203,7 +2209,7 @@ public void UpdateMissingAlias() { var query = "UPDATE account SET primarycontactid = c.contactid FROM account AS a INNER JOIN contact AS c ON a.name = c.fullname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); } @@ -2214,7 +2220,7 @@ public void UpdateMissingAliasAmbiguous() try { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); Assert.Fail("Expected exception"); } @@ -2229,7 +2235,7 @@ public void ConvertIntToBool() { var query = "UPDATE new_customentity SET new_boolprop = CASE WHEN new_name = 'True' THEN 1 ELSE 0 END"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); } @@ -2240,7 +2246,7 @@ public void ImpersonateRevert() EXECUTE AS LOGIN = 'test1' REVERT"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); Assert.IsInstanceOfType(queries[0], typeof(ExecuteAsNode)); @@ -2265,7 +2271,7 @@ SELECT contact.fullname FROM contact INNER JOIN account ON contact.contactid = account.primarycontactid INNER JOIN new_customentity ON contact.parentcustomerid = new_customentity.new_parentid ORDER BY account.employees, contact.fullname"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(new[] { queries[0] }, @" @@ -2350,12 +2356,12 @@ INNER JOIN new_customentity private void BuildTDSQuery(Action action) { - var ds = _localDataSource["local"]; + var ds = _localDataSources["local"]; var con = ds.Connection; ds.Connection = null; try { - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, new OptionsWrapper(this) { UseTDSEndpoint = true }); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, new OptionsWrapper(this) { UseTDSEndpoint = true }); action(planBuilder); } finally @@ -2369,7 +2375,7 @@ public void OrderByAggregateByIndex() { var query = "SELECT firstname, count(*) FROM contact GROUP BY firstname ORDER BY 2"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -2388,7 +2394,7 @@ public void OrderByAggregateJoinByIndex() { var query = "SELECT firstname, count(*) FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid GROUP BY firstname ORDER BY 2"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); AssertFetchXml(queries, @" @@ -2409,7 +2415,7 @@ public void AggregateAlternativeDoesNotOrderByLinkEntity() { var query = "SELECT name, count(*) FROM contact INNER JOIN account ON contact.parentcustomerid = account.accountid GROUP BY name"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var select = (SelectNode)queries[0]; @@ -2439,7 +2445,7 @@ public void CharIndex() { var query = "SELECT CHARINDEX('a', fullname) AS ci0, CHARINDEX('a', fullname, 1) AS ci1, CHARINDEX('a', fullname, 2) AS ci2, CHARINDEX('a', fullname, 3) AS ci3, CHARINDEX('a', fullname, 8) AS ci8 FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var contact1 = Guid.NewGuid(); @@ -2469,7 +2475,7 @@ public void CastDateTimeToDate() { var query = "SELECT CAST(createdon AS date) AS converted FROM contact"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var contact1 = Guid.NewGuid(); @@ -2495,7 +2501,7 @@ public void GroupByPrimaryFunction() { var query = "SELECT left(firstname, 1) AS initial, count(*) AS count FROM contact GROUP BY left(firstname, 1) ORDER BY 2 DESC"; - var planBuilder = new ExecutionPlanBuilder(_localDataSource.Values, this); + var planBuilder = new ExecutionPlanBuilder(_localDataSources.Values, this); var queries = planBuilder.Build(query, null, out _); var contact1 = Guid.NewGuid(); diff --git a/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs b/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs index a265a3b0..fbb0655b 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs +++ b/MarkMpn.Sql4Cds.Engine.Tests/StubOptions.cs @@ -25,12 +25,8 @@ class StubOptions : IQueryExecutionOptions int IQueryExecutionOptions.MaxDegreeOfParallelism => 10; - bool IQueryExecutionOptions.ColumnComparisonAvailable => true; - bool IQueryExecutionOptions.UseLocalTimeZone => false; - List IQueryExecutionOptions.JoinOperatorsAvailable => new List { JoinOperator.Inner, JoinOperator.LeftOuter }; - bool IQueryExecutionOptions.BypassCustomPlugins => false; ColumnOrdering IQueryExecutionOptions.ColumnOrdering => ColumnOrdering.Alphabetical; diff --git a/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs index 95c990db..5598817b 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/CancellationTokenOptionsWrapper.cs @@ -33,12 +33,8 @@ public CancellationTokenOptionsWrapper(IQueryExecutionOptions options, Cancellat public int MaxDegreeOfParallelism => _options.MaxDegreeOfParallelism; - public bool ColumnComparisonAvailable => _options.ColumnComparisonAvailable; - public bool UseLocalTimeZone => _options.UseLocalTimeZone; - public List JoinOperatorsAvailable => _options.JoinOperatorsAvailable; - public bool BypassCustomPlugins => _options.BypassCustomPlugins; public string PrimaryDataSource => _options.PrimaryDataSource; diff --git a/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs b/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs index b764273a..0c89a9ec 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/ChangeDatabaseOptionsWrapper.cs @@ -24,7 +24,6 @@ public ChangeDatabaseOptionsWrapper(Sql4CdsConnection connection, IQueryExecutio UseBulkDelete = options.UseBulkDelete; BatchSize = options.BatchSize; MaxDegreeOfParallelism = options.MaxDegreeOfParallelism; - ColumnComparisonAvailable = options.ColumnComparisonAvailable; UseLocalTimeZone = options.UseLocalTimeZone; BypassCustomPlugins = options.BypassCustomPlugins; QuotedIdentifiers = options.QuotedIdentifiers; @@ -45,12 +44,8 @@ public ChangeDatabaseOptionsWrapper(Sql4CdsConnection connection, IQueryExecutio public int MaxDegreeOfParallelism { get; set; } - public bool ColumnComparisonAvailable { get; } - public bool UseLocalTimeZone { get; set; } - public List JoinOperatorsAvailable => _options.JoinOperatorsAvailable; - public bool BypassCustomPlugins { get; set; } public string PrimaryDataSource { get; set; } // TODO: Update UserId when changing data source diff --git a/MarkMpn.Sql4Cds.Engine/Ado/DefaultQueryExecutionOptions.cs b/MarkMpn.Sql4Cds.Engine/Ado/DefaultQueryExecutionOptions.cs index 75e22104..295d595f 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/DefaultQueryExecutionOptions.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/DefaultQueryExecutionOptions.cs @@ -21,45 +21,23 @@ public DefaultQueryExecutionOptions(DataSource dataSource, CancellationToken can PrimaryDataSource = dataSource.Name; CancellationToken = cancellationToken; - Version version; - #if NETCOREAPP if (dataSource.Connection is ServiceClient svc) { UserId = svc.GetMyUserId(); - version = svc.ConnectedOrgVersion; } #else if (dataSource.Connection is CrmServiceClient svc) { UserId = svc.GetMyCrmUserId(); - version = svc.ConnectedOrgVersion; } #endif else { var whoami = (WhoAmIResponse)dataSource.Connection.Execute(new WhoAmIRequest()); UserId = whoami.UserId; - - var ver = (RetrieveVersionResponse)dataSource.Connection.Execute(new RetrieveVersionRequest()); - version = new Version(ver.Version); - } - - var joinOperators = new List - { - JoinOperator.Inner, - JoinOperator.LeftOuter - }; - - 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.Exists); } - JoinOperatorsAvailable = joinOperators; - ColumnComparisonAvailable = version >= new Version("9.1.0.19251"); } public CancellationToken CancellationToken { get; } @@ -78,6 +56,8 @@ public DefaultQueryExecutionOptions(DataSource dataSource, CancellationToken can public bool ColumnComparisonAvailable { get; } + public bool OrderByEntityNameAvailable { get; } + public bool UseLocalTimeZone => false; public List JoinOperatorsAvailable { get; } diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs index 32acad82..5f94b389 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs @@ -278,11 +278,11 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) foreach (Sql4CdsParameter sql4cdsParam in Parameters) { - if (!requiredParameters.Contains(sql4cdsParam.ParameterName)) + if (!requiredParameters.Contains(sql4cdsParam.FullParameterName)) continue; var param = cmd.CreateParameter(); - param.ParameterName = sql4cdsParam.ParameterName; + param.ParameterName = sql4cdsParam.FullParameterName; if (sql4cdsParam.Value is SqlEntityReference er) param.Value = (SqlGuid)er; @@ -304,7 +304,7 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { // Capture the values of output parameters foreach (var param in Parameters.Cast().Where(p => p.Direction == ParameterDirection.Output)) - param.SetOutputValue((INullable)reader.ParameterValues[param.ParameterName]); + param.SetOutputValue((INullable)reader.ParameterValues[param.FullParameterName]); } return reader; diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs index 7c483c57..5dd71eef 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs @@ -54,6 +54,8 @@ public override DbType DbType public override string ParameterName { get; set; } + internal string FullParameterName => ParameterName.StartsWith("@") ? ParameterName : ("@" + ParameterName); + public override int Size { get; set; } public override string SourceColumn { get; set; } diff --git a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs index 97eb9abb..c5ea90eb 100644 --- a/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs +++ b/MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs @@ -47,14 +47,14 @@ internal Dictionary GetParameterTypes() { return _parameters .Cast() - .ToDictionary(param => param.ParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase); + .ToDictionary(param => param.FullParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase); } internal Dictionary GetParameterValues() { return _parameters .Cast() - .ToDictionary(param => param.ParameterName, param => (object) param.GetValue(), StringComparer.OrdinalIgnoreCase); + .ToDictionary(param => param.FullParameterName, param => (object) param.GetValue(), StringComparer.OrdinalIgnoreCase); } public override bool Contains(string value) diff --git a/MarkMpn.Sql4Cds.Engine/DataSource.cs b/MarkMpn.Sql4Cds.Engine/DataSource.cs index 0c9396dc..bd546a85 100644 --- a/MarkMpn.Sql4Cds.Engine/DataSource.cs +++ b/MarkMpn.Sql4Cds.Engine/DataSource.cs @@ -2,6 +2,9 @@ using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using System; +using System.Collections.Generic; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; +using Microsoft.Crm.Sdk.Messages; #if NETCOREAPP using Microsoft.PowerPlatform.Dataverse.Client; #else @@ -21,33 +24,70 @@ public class DataSource /// Creates a new using default values based on an existing connection. /// /// The that provides the connection to the instance - 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; #if NETCOREAPP if (org is ServiceClient svc) { name = svc.ConnectedOrgUniqueName; + version = svc.ConnectedOrgVersion; } #else if (org is CrmServiceClient svc) { name = svc.ConnectedOrgUniqueName; + version = svc.ConnectedOrgVersion; } #endif - + if (name == null) { var orgDetails = org.RetrieveMultiple(new QueryExpression("organization") { ColumnSet = new ColumnSet("name") }).Entities[0]; name = orgDetails.GetAttributeValue("name"); } + if (version == null) + { + var ver = (RetrieveVersionResponse)org.Execute(new RetrieveVersionRequest()); + version = new Version(ver.Version); + } + 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.Inner, + JoinOperator.LeftOuter + }; + + 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.In); + joinOperators.Add(JoinOperator.Exists); + joinOperators.Add(JoinOperator.Any); + joinOperators.Add(JoinOperator.NotAny); + joinOperators.Add(JoinOperator.All); + joinOperators.Add(JoinOperator.NotAll); + } + + JoinOperatorsAvailable = joinOperators; + ColumnComparisonAvailable = version >= new Version("9.1.0.19251"); + OrderByEntityNameAvailable = version >= new Version("9.1.0.25249"); } /// @@ -87,6 +127,21 @@ public DataSource() /// public string SessionToken { get; set; } + /// + /// Indicates if the server supports column comparison conditions in FetchXML + /// + public virtual bool ColumnComparisonAvailable { get; } + + /// + /// Indicates if the server supports ordering by link-entities in FetchXML + /// + public virtual bool OrderByEntityNameAvailable { get; } + + /// + /// Returns a list of join operators that are supported by the server + /// + public virtual List JoinOperatorsAvailable { get; } + /// /// Returns the default collation used by this instance /// diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs index 836f5bda..b8880111 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseDataNode.cs @@ -23,6 +23,27 @@ namespace MarkMpn.Sql4Cds.Engine.ExecutionPlan /// abstract class BaseDataNode : BaseNode, IDataExecutionPlanNodeInternal { + /// + /// Holds data about a subquery filter (IN, EXISTS) that is needed to process the multi-step conversion + /// + protected class ConvertedSubquery + { + /// + /// The join that is used to process the subquery + /// + public BaseJoinNode JoinNode { get; set; } + + /// + /// The FetchXML equivalent of the subquery + /// + public FetchLinkEntityType Condition { get; set; } + + /// + /// The link entity to add the to + /// + public FetchLinkEntityType LinkEntity { get; set; } + } + private int _executionCount; private readonly Timer _timer = new Timer(); private TimeSpan _additionalDuration; @@ -194,7 +215,7 @@ public void MergeStatsFrom(BaseDataNode other) /// Translates filter criteria from ScriptDom to FetchXML /// /// The context the query is being built in - /// The to use to get metadata + /// The details of the data source the FetchXML will be executed against /// The SQL criteria to attempt to translate to FetchXML /// The schema of the node that the criteria apply to /// The prefix of the table that the can be translated for, or null if any tables can be referenced @@ -204,9 +225,9 @@ public void MergeStatsFrom(BaseDataNode other) /// The child items of the root entity in the FetchXML query /// The FetchXML version of the that is generated by this method /// true if the can be translated to FetchXML, or false otherwise - protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttributeMetadataCache metadata, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, out filter filter) + protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, Dictionary subqueryExpressions, HashSet replacedSubqueryExpression, out filter filter) { - if (!TranslateFetchXMLCriteria(context, metadata, 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) @@ -219,7 +240,7 @@ protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttrib /// Translates filter criteria from ScriptDom to FetchXML /// /// The context the query is being built in - /// The to use to get metadata + /// The details of the data source the FetchXML will be executed against /// The SQL criteria to attempt to translate to FetchXML /// The schema of the node that the criteria apply to /// The prefix of the table that the can be translated for, or null if any tables can be referenced @@ -230,16 +251,34 @@ protected bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttrib /// The FetchXML version of the that is generated by this method when it covers multiple conditions /// The FetchXML version of the that is generated by this method when it is for a single condition only /// true if the can be translated to FetchXML, or false otherwise - private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttributeMetadataCache metadata, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet 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 barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, Dictionary subqueryExpressions, HashSet 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, metadata, 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, metadata, 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 @@ -256,7 +295,7 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (criteria is BooleanParenthesisExpression paren) { - return TranslateFetchXMLCriteria(context, metadata, 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) @@ -290,7 +329,7 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut { // The operator is comparing two attributes. This is allowed in join criteria, // but not in filter conditions before version 9.1.0.19251 - if (!context.Options.ColumnComparisonAvailable) + if (!dataSource.ColumnComparisonAvailable) return false; } @@ -330,7 +369,7 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut var expressionContext = new ExpressionCompilationContext(context, schema, null); // If we still couldn't find the column name and value, this isn't a pattern we can support in FetchXML - if (field == null || (literal == null && func == null && variable == null && parameterless == null && globalVariable == null && (field2 == null || !context.Options.ColumnComparisonAvailable) && !expr.IsConstantValueExpression(expressionContext, out literal))) + if (field == null || (literal == null && func == null && variable == null && parameterless == null && globalVariable == null && (field2 == null || !dataSource.ColumnComparisonAvailable) && !expr.IsConstantValueExpression(expressionContext, out literal))) return false; // Select the correct FetchXML operator @@ -487,15 +526,14 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (IsInvalidAuditFilter(targetEntityName, entityName, items)) return false; - var meta = metadata[entityName]; + var meta = dataSource.Metadata[entityName]; if (field2 == null) { - return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, type, op, values, metadata, targetEntityAlias, items, out condition, out filter); + return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, type, op, values, dataSource, targetEntityAlias, items, out condition, out filter); } else { - // Column comparisons can only happen within a single entity var columnName2 = field2.GetColumnName(); if (!schema.ContainsColumn(columnName2, out columnName2)) return false; @@ -503,12 +541,11 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut var parts2 = columnName2.SplitMultiPartIdentifier(); var entityAlias2 = parts2[0]; var attrName2 = parts2[1]; - - if (!entityAlias.Equals(entityAlias2, StringComparison.OrdinalIgnoreCase)) - return false; + var entityName2 = AliasToEntityName(targetEntityAlias, targetEntityName, items, entityAlias2); + var meta2 = dataSource.Metadata[entityName2]; var attr1 = meta.Attributes.SingleOrDefault(a => a.LogicalName.Equals(attrName, StringComparison.OrdinalIgnoreCase)); - var attr2 = meta.Attributes.SingleOrDefault(a => a.LogicalName.Equals(attrName2, StringComparison.OrdinalIgnoreCase)); + var attr2 = meta2.Attributes.SingleOrDefault(a => a.LogicalName.Equals(attrName2, StringComparison.OrdinalIgnoreCase)); if (!String.IsNullOrEmpty(attr1?.AttributeOf)) return false; @@ -516,12 +553,29 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (!String.IsNullOrEmpty(attr2?.AttributeOf)) return false; + // We can use valueof="alias.attribute", but the alias of the root entity isn't visible. Swap the comparison round + // so that it can be added to the root entity and reference the value from the link entity. + if (!entityAlias.Equals(entityAlias2, StringComparison.OrdinalIgnoreCase) && entityAlias2.Equals(targetEntityAlias, StringComparison.OrdinalIgnoreCase)) + { + (entityAlias, entityAlias2) = (entityAlias2, entityAlias); + (attrName, attrName2) = (attrName2, attrName); + switch (op) + { + case @operator.eq: break; + case @operator.ne: break; + case @operator.lt: op = @operator.gt; break; + case @operator.le: op = @operator.ge; break; + case @operator.gt: op = @operator.lt; break; + case @operator.ge: op = @operator.le; break; + } + } + condition = new condition { entityname = StandardizeAlias(entityAlias, targetEntityAlias, items), attribute = RemoveAttributeAlias(attrName, entityAlias, targetEntityAlias, items), @operator = op, - ValueOf = RemoveAttributeAlias(attrName2, entityAlias, targetEntityAlias, items) + ValueOf = (entityAlias.Equals(entityAlias2, StringComparison.OrdinalIgnoreCase) ? "" : $"{StandardizeAlias(entityAlias2, targetEntityAlias, items)}.") + RemoveAttributeAlias(attrName2, entityAlias, targetEntityAlias, items) }; if (op == @operator.ne && (type == BooleanComparisonType.NotEqualToBrackets || type == BooleanComparisonType.NotEqualToExclamation)) @@ -617,9 +671,9 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (IsInvalidAuditFilter(targetEntityName, entityName, items)) return false; - var meta = metadata[entityName]; + var meta = dataSource.Metadata[entityName]; - return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, inPred.NotDefined ? @operator.notin : @operator.@in, inPred.Values.Cast().ToArray(), metadata, targetEntityAlias, items, out condition, out filter); + return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, inPred.NotDefined ? @operator.notin : @operator.@in, inPred.Values.Cast().ToArray(), dataSource, targetEntityAlias, items, out condition, out filter); } if (criteria is BooleanIsNullExpression isNull) @@ -644,9 +698,9 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (IsInvalidAuditFilter(targetEntityName, entityName, items)) return false; - var meta = metadata[entityName]; + var meta = dataSource.Metadata[entityName]; - return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, isNull.IsNot ? @operator.notnull : @operator.@null, null, metadata, targetEntityAlias, items, out condition, out filter); + return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, isNull.IsNot ? @operator.notnull : @operator.@null, null, dataSource, targetEntityAlias, items, out condition, out filter); } if (criteria is LikePredicate like) @@ -677,9 +731,9 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (IsInvalidAuditFilter(targetEntityName, entityName, items)) return false; - var meta = metadata[entityName]; + var meta = dataSource.Metadata[entityName]; - return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, like.NotDefined ? @operator.notlike : @operator.like, new[] { value }, metadata, targetEntityAlias, items, out condition, out filter); + return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, like.NotDefined ? @operator.notlike : @operator.like, new[] { value }, dataSource, targetEntityAlias, items, out condition, out filter); } if (criteria is FullTextPredicate || @@ -724,13 +778,13 @@ private bool TranslateFetchXMLCriteria(NodeCompilationContext context, IAttribut if (IsInvalidAuditFilter(targetEntityName, entityName, items)) return false; - var meta = metadata[entityName]; + var meta = dataSource.Metadata[entityName]; var attr = meta.Attributes.Single(a => a.LogicalName.Equals(attrName)); if (!(attr is MultiSelectPicklistAttributeMetadata)) return false; - return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, not == null ? @operator.containvalues : @operator.notcontainvalues, valueParts.Select(v => new IntegerLiteral { Value = v }).ToArray(), metadata, targetEntityAlias, items, out condition, out filter); + return TranslateFetchXMLCriteriaWithVirtualAttributes(context, meta, entityAlias, attrName, null, not == null ? @operator.containvalues : @operator.notcontainvalues, valueParts.Select(v => new IntegerLiteral { Value = v }).ToArray(), dataSource, targetEntityAlias, items, out condition, out filter); } return false; @@ -773,13 +827,13 @@ private bool IsInvalidAuditFilter(string targetEntityName, string entityName, ob /// The original SQL comparison type /// The condition operator to apply /// The values to compare the attribute value to - /// The to use to get metadata + /// The details of the data source the FetchXML will be executed against /// The alias of the root entity that the FetchXML query is targetting /// The child items of the root entity in the FetchXML query /// The FetchXML version of the that is generated by this method when it covers multiple conditions /// The FetchXML version of the that is generated by this method when it is for a single condition only /// true if the condition can be translated to FetchXML, or false otherwise - private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationContext context, EntityMetadata meta, string entityAlias, string attrName, BooleanComparisonType? type, @operator op, ValueExpression[] literals, IAttributeMetadataCache metadata, string targetEntityAlias, object[] items, out condition condition, out filter filter) + private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationContext context, EntityMetadata meta, string entityAlias, string attrName, BooleanComparisonType? type, @operator op, ValueExpression[] literals, DataSource dataSource, string targetEntityAlias, object[] items, out condition condition, out filter filter) { condition = null; filter = null; @@ -1027,9 +1081,14 @@ private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationConte // it's not always the same under the hood. if (attributeSuffix == "name") { + // Should normally only be one string virtual attribute related to the lookup attribute and one yomi one. Sometimes + // the yomi version is not flagged as such, and some special cases have additional ones as well + // https://github.com/MarkMpn/Sql4Cds/issues/443 attribute = meta.Attributes .OfType() - .SingleOrDefault(a => a.AttributeOf == attrName && a.AttributeType == AttributeTypeCode.String && a.YomiOf == null); + .Where(a => a.AttributeOf == attrName && a.AttributeType == AttributeTypeCode.String && a.YomiOf == null) + .OrderBy(a => a.LogicalName == attrName + "name" ? 0 : 1) + .FirstOrDefault(); } else { @@ -1082,7 +1141,7 @@ private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationConte try { // Convert the literal entity name to the object type code - var targetMetadata = metadata[values[i].Value]; + var targetMetadata = dataSource.Metadata[values[i].Value]; values[i].Value = targetMetadata.ObjectTypeCode?.ToString(); } catch @@ -1112,7 +1171,8 @@ private bool TranslateFetchXMLCriteriaWithVirtualAttributes(NodeCompilationConte if (meta.LogicalName == "solution" && linkName != null && !new FetchEntityType { Items = items }.GetLinkEntities(true).Any(link => link.alias == linkName)) return false; - if ((op == @operator.ne || op == @operator.nebusinessid || op == @operator.neq || op == @operator.neuserid) && type != BooleanComparisonType.IsDistinctFrom) + if ((op == @operator.ne || op == @operator.nebusinessid || op == @operator.neq || op == @operator.neuserid || op == @operator.notlike) + && type != BooleanComparisonType.IsDistinctFrom) { // FetchXML not-equal type operators treat NULL as not-equal to values, but T-SQL treats them as not-not-equal. Add // an extra not-null condition to keep it compatible with T-SQL diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseJoinNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseJoinNode.cs index 5c85ec19..cc8e64b2 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseJoinNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/BaseJoinNode.cs @@ -75,30 +75,36 @@ abstract class BaseJoinNode : BaseDataNode /// The data from the right source /// The schema of the right source /// The merged data - protected Entity Merge(Entity leftEntity, INodeSchema leftSchema, Entity rightEntity, INodeSchema rightSchema) + protected Entity Merge(Entity leftEntity, INodeSchema leftSchema, Entity rightEntity, INodeSchema rightSchema, bool includeSemiJoin) { var merged = new Entity(); - if (leftEntity != null) + if (OutputLeftSchema || includeSemiJoin) { - foreach (var attr in leftSchema.Schema) - merged[attr.Key] = leftEntity[attr.Key]; - } - else - { - foreach (var attr in leftSchema.Schema) - merged[attr.Key] = SqlTypeConverter.GetNullValue(attr.Value.Type.ToNetType(out _)); + if (leftEntity != null) + { + foreach (var attr in leftSchema.Schema) + merged[attr.Key] = leftEntity[attr.Key]; + } + else + { + foreach (var attr in leftSchema.Schema) + merged[attr.Key] = SqlTypeConverter.GetNullValue(attr.Value.Type.ToNetType(out _)); + } } - if (rightEntity != null) + if (OutputRightSchema || includeSemiJoin) { - foreach (var attr in rightSchema.Schema) - merged[attr.Key] = rightEntity[attr.Key]; - } - else - { - foreach (var attr in rightSchema.Schema) - merged[attr.Key] = SqlTypeConverter.GetNullValue(attr.Value.Type.ToNetType(out _)); + if (rightEntity != null) + { + foreach (var attr in rightSchema.Schema) + merged[attr.Key] = rightEntity[attr.Key]; + } + else + { + foreach (var attr in rightSchema.Schema) + merged[attr.Key] = SqlTypeConverter.GetNullValue(attr.Value.Type.ToNetType(out _)); + } } foreach (var definedValue in DefinedValues) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ConditionalNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ConditionalNode.cs index 871967c2..89ffee06 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ConditionalNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ConditionalNode.cs @@ -70,14 +70,6 @@ public IRootExecutionPlanNodeInternal[] FoldQuery(NodeCompilationContext context { Source = Source?.FoldQuery(context, hints); - TrueStatements = TrueStatements - .SelectMany(s => s.FoldQuery(context, hints)) - .ToArray(); - - FalseStatements = FalseStatements - ?.SelectMany(s => s.FoldQuery(context, hints)) - ?.ToArray(); - return new[] { this }; } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs index c9fc9fa4..5bbf37d6 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FetchXmlScan.cs @@ -372,7 +372,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont // Archive queries can fail with this error code if the Synapse database isn't provisioned yet or // no retention policy has yet been applied to this table. In either case there are no records to return // so we can just return an empty result set rather than erroring - if (FetchXml.DataSource == "archive" && (ex.Detail.ErrorCode == -2146863832 || ex.Detail.ErrorCode == -2146863829)) + if (FetchXml.DataSource == "retained" && (ex.Detail.ErrorCode == -2146863832 || ex.Detail.ErrorCode == -2146863829)) yield break; throw; @@ -580,6 +580,18 @@ public void RemoveSorts() } } + public void RemoveAttributes() + { + // Remove any existing sorts + if (Entity.Items != null) + { + Entity.Items = Entity.Items.Where(i => !(i is FetchAttributeType) && !(i is allattributes)).ToArray(); + + foreach (var linkEntity in Entity.GetLinkEntities().Where(le => le.Items != null)) + linkEntity.Items = linkEntity.Items.Where(i => !(i is FetchAttributeType) && !(i is allattributes)).ToArray(); + } + } + private void OnRetrievedEntity(Entity entity, INodeSchema schema, IQueryExecutionOptions options, DataSource dataSource) { // Expose any formatted values for OptionSetValue and EntityReference values @@ -1159,7 +1171,7 @@ private void AddSchemaAttributes(NodeCompilationContext context, DataSource data foreach (var linkEntity in items.OfType()) { - if (linkEntity.SemiJoin) + if (linkEntity.SemiJoin || linkEntity.linktype == "in" || linkEntity.linktype == "exists") continue; if (primaryKey != null) @@ -1211,7 +1223,10 @@ private void AddNotNullFilters(ColumnList schema, Dictionary()) { - if (cond.@operator == @operator.@null || cond.@operator == @operator.ne || cond.@operator == @operator.nebusinessid || cond.@operator == @operator.neq || cond.@operator == @operator.neuserid) + if (cond.@operator == @operator.@null || cond.@operator == @operator.ne || cond.@operator == @operator.nebusinessid || + cond.@operator == @operator.neq || cond.@operator == @operator.neuserid || cond.@operator == @operator.notlike || + cond.@operator == @operator.notin || cond.@operator == @operator.notunder || cond.@operator == @operator.notbeginwith || + cond.@operator == @operator.notbetween || cond.@operator == @operator.notcontainvalues || cond.@operator == @operator.notendwith) continue; var fullname = (cond.entityname?.EscapeIdentifier() ?? alias) + "." + (cond.alias ?? cond.attribute).EscapeIdentifier(); @@ -1519,7 +1534,10 @@ private void MoveFiltersToLinkEntities() { // If we've got AND-ed conditions that have an entityname that refers to an inner-joined link entity, move // the condition to that link entity - var innerLinkEntities = Entity.GetLinkEntities(innerOnly: true).ToDictionary(le => le.alias, StringComparer.OrdinalIgnoreCase); + var innerLinkEntities = Entity + .GetLinkEntities(innerOnly: true) + .Where(le => le.alias != null) + .ToDictionary(le => le.alias, StringComparer.OrdinalIgnoreCase); Entity.Items = MoveFiltersToLinkEntities(innerLinkEntities, Entity.Items); Entity.Items = MoveConditionsToLinkEntities(innerLinkEntities, Entity.Items); @@ -1695,8 +1713,8 @@ private void MergeSingleConditionFilters(filter filter) { var singleConditionFilters = filter.Items .OfType() - .Where(f => f.Items != null && f.Items.Length == 1 && f.Items.OfType().Count() == 1) - .ToDictionary(f => f, f => (condition)f.Items[0]); + .Where(f => f.Items != null && f.Items.Length == 1 && f.Items.Where(x => !(x is filter)).Count() == 1) + .ToDictionary(f => f, f => f.Items[0]); for (var i = 0; i < filter.Items.Length; i++) { diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs index 05d4e76b..9f0b8d55 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FilterNode.cs @@ -107,12 +107,12 @@ private void AddNotNullColumns(NodeSchema schema, BooleanExpression filter, bool AddNotNullColumns(schema, n.Expression, !not); } - if (!not && filter is InPredicate inPred) + if (!not && filter is InPredicate inPred && !inPred.NotDefined) { AddNotNullColumn(schema, inPred.Expression); } - if (!not && filter is LikePredicate like) + if (!not && filter is LikePredicate like && !like.NotDefined) { AddNotNullColumn(schema, like.FirstExpression); AddNotNullColumn(schema, like.SecondExpression); @@ -152,10 +152,10 @@ public override IDataExecutionPlanNodeInternal FoldQuery(NodeCompilationContext foldedFilters |= FoldConsecutiveFilters(); foldedFilters |= FoldNestedLoopFiltersToJoins(context, hints); - foldedFilters |= FoldInExistsToFetchXml(context, hints, out var addedLinks); + foldedFilters |= FoldInExistsToFetchXml(context, hints, out var addedLinks, out var subqueryConditions); foldedFilters |= FoldTableSpoolToIndexSpool(context, hints); foldedFilters |= ExpandFiltersOnColumnComparisons(context); - foldedFilters |= FoldFiltersToDataSources(context, hints); + foldedFilters |= FoldFiltersToDataSources(context, hints, subqueryConditions); foreach (var addedLink in addedLinks) { @@ -717,7 +717,7 @@ private void Swap(ref T first, ref T second) second = temp; } - private bool FoldInExistsToFetchXml(NodeCompilationContext context, IList hints, out Dictionary addedLinks) + private bool FoldInExistsToFetchXml(NodeCompilationContext context, IList hints, out Dictionary addedLinks, out Dictionary subqueryConditions) { var foldedFilters = false; @@ -747,6 +747,8 @@ private bool FoldInExistsToFetchXml(NodeCompilationContext context, IList(); + subqueryConditions = new Dictionary(); + FetchXmlScan leftFetch; if (joins.Count == 0) @@ -812,6 +814,8 @@ private bool FoldInExistsToFetchXml(NodeCompilationContext context, IList hints) + private bool FoldFiltersToDataSources(NodeCompilationContext context, IList hints, Dictionary subqueryExpressions) { var foldedFilters = false; @@ -1133,7 +1201,16 @@ private bool FoldFiltersToDataSources(NodeCompilationContext context, IList> GetIgnoreAliasesByNode(NodeCompilationContext context) { var fetchXmlSources = GetFoldableSources(Source, context) @@ -1282,7 +1378,10 @@ private IEnumerable GetAliases(FetchXmlScan fetchXml) yield return fetchXml.Alias; foreach (var linkEntity in fetchXml.Entity.GetLinkEntities()) - yield return linkEntity.alias; + { + if (linkEntity.alias != null) + yield return linkEntity.alias; + } } private BooleanExpression ReplaceColumnNames(BooleanExpression filter, Dictionary replacements) @@ -1455,9 +1554,62 @@ public override void AddRequiredColumns(NodeCompilationContext context, IList barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, out filter filter) + private BooleanExpression ExtractFetchXMLFilters(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet barredPrefixes, FetchXmlScan fetchXmlScan, Dictionary subqueryExpressions, out filter filter) + { + var targetEntityName = fetchXmlScan.Entity.name; + var targetEntityAlias = fetchXmlScan.Alias; + var items = fetchXmlScan.Entity.Items; + + var subqueryConditions = new HashSet(); + var result = ExtractFetchXMLFilters(context, dataSource, criteria, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, subqueryConditions, out filter); + + if (result == criteria) + return result; + + // If we've used any subquery expressions we need to make sure all the conditions are for the same entity as we + // can't specify an entityname attribute for the subquery filter. + if (subqueryConditions.Count == 0) + return result; + + var subqueryLinks = subqueryConditions + .Select(c => subqueryExpressions[c].LinkEntity?.alias ?? targetEntityAlias) + .Distinct() + .ToList(); + + if (subqueryLinks.Count > 1) + { + filter = null; + return criteria; + } + + foreach (var condition in filter.GetConditions()) + { + if ((condition.entityname ?? targetEntityAlias) != subqueryLinks[0]) + { + filter = null; + return criteria; + } + + condition.entityname = null; + } + + foreach (var subqueryCondition in subqueryConditions) + RemoveJoin(subqueryExpressions[subqueryCondition].JoinNode); + + // If the criteria are to be applied to the root entity, no need to do any further processing + if (subqueryLinks[0] == targetEntityAlias) + return result; + + // Otherwise, add the filter directly to the link entity + var linkEntity = fetchXmlScan.Entity.FindLinkEntity(subqueryLinks[0]); + linkEntity.AddItem(filter); + filter = null; + return result; + } + + private BooleanExpression ExtractFetchXMLFilters(NodeCompilationContext context, DataSource dataSource, BooleanExpression criteria, INodeSchema schema, string allowedPrefix, HashSet barredPrefixes, string targetEntityName, string targetEntityAlias, object[] items, Dictionary subqueryExpressions, HashSet replacedSubqueryExpressions, out filter filter) { - if (TranslateFetchXMLCriteria(context, metadata, criteria, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out filter)) + if (TranslateFetchXMLCriteria(context, dataSource, criteria, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpressions, out filter)) return null; if (!(criteria is BooleanBinaryExpression bin)) @@ -1466,8 +1618,8 @@ private BooleanExpression ExtractFetchXMLFilters(NodeCompilationContext context, if (bin.BinaryExpressionType != BooleanBinaryExpressionType.And) return criteria; - bin.FirstExpression = ExtractFetchXMLFilters(context, metadata, bin.FirstExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out var lhsFilter); - bin.SecondExpression = ExtractFetchXMLFilters(context, metadata, bin.SecondExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, out var rhsFilter); + bin.FirstExpression = ExtractFetchXMLFilters(context, dataSource, bin.FirstExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpressions, out var lhsFilter); + bin.SecondExpression = ExtractFetchXMLFilters(context, dataSource, bin.SecondExpression, schema, allowedPrefix, barredPrefixes, targetEntityName, targetEntityAlias, items, subqueryExpressions, replacedSubqueryExpressions, out var rhsFilter); filter = (lhsFilter != null && rhsFilter != null) ? new filter { Items = new object[] { lhsFilter, rhsFilter } } : lhsFilter ?? rhsFilter; diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FoldableJoinNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FoldableJoinNode.cs index 11a3894b..c80364fc 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FoldableJoinNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/FoldableJoinNode.cs @@ -278,7 +278,7 @@ private bool FoldFetchXmlJoin(NodeCompilationContext context, IList(ref T left, ref T right) public override void AddRequiredColumns(NodeCompilationContext context, IList requiredColumns) { - if (AdditionalJoinCriteria != null) - { - foreach (var col in AdditionalJoinCriteria.GetColumns()) - { - if (!requiredColumns.Contains(col, StringComparer.OrdinalIgnoreCase)) - requiredColumns.Add(col); - } - } + var criteriaCols = AdditionalJoinCriteria?.GetColumns() ?? Enumerable.Empty(); // Work out which columns need to be pushed down to which source var leftSchema = LeftSource.GetSchema(context); var rightSchema = RightSource.GetSchema(context); - var leftColumns = requiredColumns + var leftColumns = requiredColumns.Where(col => OutputLeftSchema) + .Concat(criteriaCols) .Where(col => leftSchema.ContainsColumn(col, out _)) - .Distinct() + .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); - var rightColumns = requiredColumns + var rightColumns = requiredColumns.Where(col => OutputRightSchema) + .Concat(criteriaCols) .Where(col => rightSchema.ContainsColumn(col, out _)) .Concat(DefinedValues.Values) - .Distinct() + .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (LeftAttribute != null) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashJoinNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashJoinNode.cs index 519ef88f..2cc1cced 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashJoinNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashJoinNode.cs @@ -87,12 +87,13 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont if (SemiJoin && left.Used) continue; - var merged = Merge(left.Entity, leftSchema, entity, rightSchema); + var finalMerged = Merge(left.Entity, leftSchema, entity, rightSchema, false); + var merged = (OutputLeftSchema && OutputRightSchema) || additionalJoinCriteria == null ? finalMerged : Merge(left.Entity, leftSchema, entity, rightSchema, true); expressionContext.Entity = merged; if (additionalJoinCriteria == null || additionalJoinCriteria(expressionContext)) { - yield return merged; + yield return finalMerged; left.Used = true; matched = true; } @@ -100,13 +101,13 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont } if (!matched && (JoinType == QualifiedJoinType.RightOuter || JoinType == QualifiedJoinType.FullOuter)) - yield return Merge(null, leftSchema, entity, rightSchema); + yield return Merge(null, leftSchema, entity, rightSchema, false); } if (JoinType == QualifiedJoinType.LeftOuter || JoinType == QualifiedJoinType.FullOuter) { foreach (var unmatched in _hashTable.SelectMany(kvp => kvp.Value).Where(e => !e.Used)) - yield return Merge(unmatched.Entity, leftSchema, null, rightSchema); + yield return Merge(unmatched.Entity, leftSchema, null, rightSchema, false); } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/MergeJoinNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/MergeJoinNode.cs index b4daeeff..76f1a87e 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/MergeJoinNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/MergeJoinNode.cs @@ -47,12 +47,22 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont var lt = LeftAttribute == null || RightAttribute == null ? ConstantResult(false) - : new BooleanComparisonExpression + : new BooleanBinaryExpression + { + FirstExpression = new BooleanBinaryExpression + { + FirstExpression = new BooleanIsNullExpression { Expression = LeftAttribute }, + BinaryExpressionType = BooleanBinaryExpressionType.And, + SecondExpression = new BooleanIsNullExpression { Expression = RightAttribute, IsNot = true } + }, + BinaryExpressionType = BooleanBinaryExpressionType.Or, + SecondExpression = new BooleanComparisonExpression { FirstExpression = LeftAttribute, ComparisonType = BooleanComparisonType.LessThan, SecondExpression = RightAttribute - }.Compile(expressionCompilationContext); + } + }.Compile(expressionCompilationContext); var eq = LeftAttribute == null || RightAttribute == null ? ConstantResult(true) @@ -65,12 +75,22 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont var gt = LeftAttribute == null || RightAttribute == null ? ConstantResult(false) - : new BooleanComparisonExpression + : new BooleanBinaryExpression + { + FirstExpression = new BooleanBinaryExpression + { + FirstExpression = new BooleanIsNullExpression { Expression = LeftAttribute, IsNot = true }, + BinaryExpressionType = BooleanBinaryExpressionType.And, + SecondExpression = new BooleanIsNullExpression { Expression = RightAttribute } + }, + BinaryExpressionType = BooleanBinaryExpressionType.Or, + SecondExpression = new BooleanComparisonExpression { FirstExpression = LeftAttribute, ComparisonType = BooleanComparisonType.GreaterThan, SecondExpression = RightAttribute - }.Compile(expressionCompilationContext); + } + }.Compile(expressionCompilationContext); string leftAttributeName = null; if (LeftAttribute != null) @@ -86,7 +106,8 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont while (!Done(hasLeft, hasRight)) { // Compare key values - var merged = Merge(hasLeft ? left.Current : null, leftSchema, hasRight ? right.Current : null, rightSchema); + var finalMerged = Merge(hasLeft ? left.Current : null, leftSchema, hasRight ? right.Current : null, rightSchema, false); + var merged = OutputLeftSchema && OutputRightSchema ? finalMerged : Merge(hasLeft ? left.Current : null, leftSchema, hasRight ? right.Current : null, rightSchema, true); expressionExecutionContext.Entity = merged; var isLt = lt(expressionExecutionContext); @@ -95,7 +116,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont var nextSide = right; - if (isLt || (hasLeft && !hasRight)) + if (hasLeft && (isLt || !hasRight)) { nextSide = left; } @@ -133,7 +154,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont if ((!leftMatched || !SemiJoin) && (additionalJoinCriteria == null || additionalJoinCriteria(expressionExecutionContext) == true)) { - yield return merged; + yield return finalMerged; leftMatched = true; rightMatched = true; workTableUnmatched.Remove(right.Current); @@ -144,7 +165,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont if (nextSide == right) { if (!rightMatched && right == rightSource && (JoinType == QualifiedJoinType.RightOuter || JoinType == QualifiedJoinType.FullOuter)) - yield return Merge(null, leftSchema, right.Current, rightSchema); + yield return Merge(null, leftSchema, right.Current, rightSchema, false); hasRight = right.MoveNext(); rightMatched = false; @@ -162,7 +183,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont if (nextSide == left) { if (!leftMatched && (JoinType == QualifiedJoinType.LeftOuter || JoinType == QualifiedJoinType.FullOuter)) - yield return Merge(left.Current, leftSchema, null, rightSchema); + yield return Merge(left.Current, leftSchema, null, rightSchema, false); // If we're using the work table, check if the key is about to change in the left input. If so, discard the // work table and move back to the right input @@ -180,7 +201,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont if (JoinType == QualifiedJoinType.RightOuter || JoinType == QualifiedJoinType.FullOuter) { foreach (var entity in workTableUnmatched) - yield return Merge(null, leftSchema, entity, rightSchema); + yield return Merge(null, leftSchema, entity, rightSchema, false); } } } diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/NestedLoopNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/NestedLoopNode.cs index fb83ad98..de391dc0 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/NestedLoopNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/NestedLoopNode.cs @@ -94,18 +94,18 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont joinConditionContext = joinCondition == null ? null : new ExpressionExecutionContext(context); } - var merged = Merge(left, leftSchema, right, rightSchema); + var finalMerged = Merge(left, leftSchema, right, rightSchema, false); if (joinCondition == null) { - yield return merged; + yield return finalMerged; } else { - joinConditionContext.Entity = merged; + joinConditionContext.Entity = OutputLeftSchema && OutputRightSchema ? finalMerged : Merge(left, leftSchema, right, rightSchema, true); if (joinCondition(joinConditionContext)) - yield return merged; + yield return finalMerged; else continue; } @@ -126,7 +126,7 @@ protected override IEnumerable ExecuteInternal(NodeExecutionContext cont joinConditionContext = joinCondition == null ? null : new ExpressionExecutionContext(context); } - yield return Merge(left, leftSchema, null, rightSchema); + yield return Merge(left, leftSchema, null, rightSchema, false); } } } @@ -315,28 +315,23 @@ applyAlias.Source is IndexSpoolNode applyIndexSpool && public override void AddRequiredColumns(NodeCompilationContext context, IList requiredColumns) { - if (JoinCondition != null) - { - foreach (var col in JoinCondition.GetColumns()) - { - if (!requiredColumns.Contains(col, StringComparer.OrdinalIgnoreCase)) - requiredColumns.Add(col); - } - } + var criteriaCols = JoinCondition?.GetColumns() ?? Enumerable.Empty(); var leftSchema = LeftSource.GetSchema(context); - var leftColumns = requiredColumns + var leftColumns = requiredColumns.Where(col => OutputLeftSchema) + .Concat(criteriaCols) .Where(col => leftSchema.ContainsColumn(col, out _)) - .Concat((IEnumerable) OuterReferences?.Keys ?? Array.Empty()) - .Distinct() + .Concat((IEnumerable) OuterReferences?.Keys ?? Enumerable.Empty()) + .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var innerParameterTypes = GetInnerParameterTypes(leftSchema, context.ParameterTypes); var innerContext = new NodeCompilationContext(context.DataSources, context.Options, innerParameterTypes, context.Log); var rightSchema = RightSource.GetSchema(innerContext); - var rightColumns = requiredColumns + var rightColumns = requiredColumns.Where(col => OutputRightSchema) + .Concat(criteriaCols) .Where(col => rightSchema.ContainsColumn(col, out _)) .Concat(DefinedValues.Values) - .Distinct() + .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); LeftSource.AddRequiredColumns(context, leftColumns); diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs index a6b69954..0859317b 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/SortNode.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using MarkMpn.Sql4Cds.Engine.FetchXml; +using Microsoft.Crm.Sdk.Messages; using Microsoft.SqlServer.TransactSql.ScriptDom; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; @@ -276,9 +277,13 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) { fetchXml.RemoveSorts(); + // Validate whether the sort orders are applied in a valid order to be added directly to the and + // elements, or if we need to use the attribute + var entityOrder = GetEntityOrder(fetchXml); var fetchSchema = fetchXml.GetSchema(context); - var entity = fetchXml.Entity; - var items = entity.Items; + var validOrder = true; + var currentEntity = 0; + bool? useRawOrderBy = null; foreach (var sortOrder in Sorts) { @@ -316,14 +321,40 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) fetchSort.attribute = null; } - if (entityName == fetchXml.Alias) + if (validOrder) { - if (items != entity.Items) - return this; + var entityIndex = entityOrder.IndexOf(entityName); + if (entityIndex < currentEntity) + { + validOrder = false; + + // We've already added sorts to a subsequent link-entity. We can only add sorts to a previous entity if + // we support the attribute + if (!dataSource.OrderByEntityNameAvailable) + return this; + + // Existing orders on link-entities need to be moved to the root entity and have their entityname attribute populated + foreach (var linkEntity in fetchXml.Entity.GetLinkEntities()) + { + foreach (var sort in linkEntity.Items?.OfType()?.ToArray() ?? Array.Empty()) + { + sort.entityname = linkEntity.alias; + linkEntity.Items = linkEntity.Items.Except(new[] { sort }).ToArray(); + fetchXml.Entity.AddItem(sort); + } + } + } + else + { + currentEntity = entityIndex; + } + } + if (entityName == fetchXml.Alias) + { if (fetchSort.attribute != null) { - var meta = dataSource.Metadata[entity.name]; + var meta = dataSource.Metadata[fetchXml.Entity.name]; var attribute = meta.Attributes.SingleOrDefault(a => a.LogicalName == fetchSort.attribute && a.AttributeOf == null); // Sorting on multi-select picklist fields isn't supported in FetchXML @@ -345,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) @@ -355,7 +395,17 @@ 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) + { fetchSort.attribute = attribute.LogicalName; + + if (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata) + { + if (useRawOrderBy == true) + return this; + + useRawOrderBy = false; + } + } } } @@ -363,12 +413,11 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) return this; } - entity.AddItem(fetchSort); - items = entity.Items; + fetchXml.Entity.AddItem(fetchSort); } else { - var linkEntity = FetchXmlExtensions.FindLinkEntity(items, entityName); + var linkEntity = fetchXml.Entity.FindLinkEntity(entityName); if (linkEntity == null) return this; @@ -377,9 +426,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) @@ -391,7 +449,17 @@ 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) + { fetchSort.attribute = attribute.LogicalName; + + if (attribute is EnumAttributeMetadata || attribute is BooleanAttributeMetadata) + { + if (useRawOrderBy == true) + return this; + + useRawOrderBy = false; + } + } } if (attribute == null) @@ -430,11 +498,21 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) } } - linkEntity.AddItem(fetchSort); - items = linkEntity.Items; + if (validOrder) + { + linkEntity.AddItem(fetchSort); + } + else + { + fetchSort.entityname = entityName; + fetchXml.Entity.AddItem(fetchSort); + } } PresortedCount++; + + if (useRawOrderBy == true) + fetchXml.FetchXml.UseRawOrderBy = true; } return Source; @@ -463,6 +541,18 @@ private IDataExecutionPlanNodeInternal FoldSorts(NodeCompilationContext context) return this; } + private List GetEntityOrder(FetchXmlScan fetchXml) + { + // Get the list of entities in the order that elements will be applied, i.e. DFS + var order = new List(); + order.Add(fetchXml.Alias); + + foreach (var linkEntity in fetchXml.Entity.GetLinkEntities()) + order.Add(linkEntity.alias); + + return order; + } + private string FindEntityWithAttributeAlias(FetchXmlScan fetchXml, string attrName) { return FindEntityWithAttributeAlias(fetchXml.Alias, fetchXml.Entity.Items, attrName); diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index f6fd3c45..38d7f4da 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -3858,7 +3858,7 @@ private bool UseMergeJoin(IDataExecutionPlanNodeInternal node, IDataExecutionPla } outputCol = subqueryCol; - subNode.AddRequiredColumns(context, new List { subqueryCol }); + //subNode.AddRequiredColumns(context, new List { subqueryCol }); } if (alias != null && !(subNode is FetchXmlScan)) @@ -4182,7 +4182,7 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe if (meta.IsRetentionEnabled != true && meta.IsArchivalEnabled != true) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 208, $"Invalid object name '{table.ToSql()}'", table)) { Suggestion = "Ensure long term retention is enabled for this table - see https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-retention-set?WT.mc_id=DX-MVP-5004203" }; - fetchXmlScan.FetchXml.DataSource = "archive"; + fetchXmlScan.FetchXml.DataSource = "retained"; } return fetchXmlScan; diff --git a/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs b/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs index ca105188..38ea6f0f 100644 --- a/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs +++ b/MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs @@ -894,7 +894,7 @@ public static object Value(SqlXml value, XPath2Expression query, [TargetType] Da else if (result is double d) sqlValue = (SqlDouble)d; else if (result is XPath2NodeIterator nodeIterator) - sqlValue = context.PrimaryDataSource.DefaultCollation.ToSqlString(nodeIterator.First().Value); + sqlValue = context.PrimaryDataSource.DefaultCollation.ToSqlString(nodeIterator.FirstOrDefault()?.Value); else throw new QueryExecutionException(new Sql4CdsError(16, 40517, $"Unsupported XPath return type '{result.GetType().Name}'")); diff --git a/MarkMpn.Sql4Cds.Engine/FetchXml.Extensions.cs b/MarkMpn.Sql4Cds.Engine/FetchXml.Extensions.cs index b4cf2244..c19c2b77 100644 --- a/MarkMpn.Sql4Cds.Engine/FetchXml.Extensions.cs +++ b/MarkMpn.Sql4Cds.Engine/FetchXml.Extensions.cs @@ -39,5 +39,15 @@ partial class FetchType [XmlAttribute("datasource")] public string DataSource { get; set; } + + [XmlAttribute("useraworderby")] + [DefaultValue(false)] + public bool UseRawOrderBy { get; set; } + } + + partial class FetchOrderType + { + [XmlAttribute] + public string entityname { get; set; } } } diff --git a/MarkMpn.Sql4Cds.Engine/FetchXml.cs b/MarkMpn.Sql4Cds.Engine/FetchXml.cs index a3befef5..3873e970 100644 --- a/MarkMpn.Sql4Cds.Engine/FetchXml.cs +++ b/MarkMpn.Sql4Cds.Engine/FetchXml.cs @@ -902,6 +902,7 @@ public filter() { /// [System.Xml.Serialization.XmlElementAttribute("condition", typeof(condition))] [System.Xml.Serialization.XmlElementAttribute("filter", typeof(filter))] + [System.Xml.Serialization.XmlElementAttribute("link-entity", typeof(FetchLinkEntityType))] public object[] Items { get { return this.itemsField; diff --git a/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs b/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs index 66777927..4f713e57 100644 --- a/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs +++ b/MarkMpn.Sql4Cds.Engine/FetchXml2Sql.cs @@ -71,6 +71,9 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m // FROM var aliasToLogicalName = new Dictionary(StringComparer.OrdinalIgnoreCase); + // Link entities can also affect the WHERE clause + var filter = GetFilter(org, metadata, entity.Items, entity.name, aliasToLogicalName, options, ctes, parameterValues, ref requiresTimeZone, ref usesToday); + if (entity != null) { query.FromClause = new FromClause @@ -90,14 +93,14 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m } }; - if (fetch.DataSource == "archive") + if (fetch.DataSource == "archive" || fetch.DataSource == "retained") ((NamedTableReference)query.FromClause.TableReferences[0]).SchemaObject.Identifiers.Insert(0, new Identifier { Value = "archive" }); if (fetch.nolock) ((NamedTableReference)query.FromClause.TableReferences[0]).TableHints.Add(new TableHint { HintKind = TableHintKind.NoLock }); // Recurse into link-entities to build joins - query.FromClause.TableReferences[0] = BuildJoins(org, metadata, query.FromClause.TableReferences[0], (NamedTableReference)query.FromClause.TableReferences[0], entity.Items, query, aliasToLogicalName, fetch.DataSource == "archive", fetch.nolock, options, ctes, parameterValues, ref requiresTimeZone, ref usesToday); + query.FromClause.TableReferences[0] = BuildJoins(org, metadata, query.FromClause.TableReferences[0], (NamedTableReference)query.FromClause.TableReferences[0], entity.Items, query, aliasToLogicalName, fetch.DataSource == "archive", fetch.nolock, options, ctes, parameterValues, ref requiresTimeZone, ref usesToday, ref filter); } // OFFSET @@ -114,7 +117,6 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m } // WHERE - var filter = GetFilter(org, metadata, entity.Items, entity.name, aliasToLogicalName, options, ctes, parameterValues, ref requiresTimeZone, ref usesToday); if (filter != null) { query.WhereClause = new WhereClause @@ -124,7 +126,7 @@ public static string Convert(IOrganizationService org, IAttributeMetadataCache m } // ORDER BY - AddOrderBy(entity.name, entity.Items, query); + AddOrderBy(entity.name, entity.Items, query, fetch.UseRawOrderBy, metadata, aliasToLogicalName); // For single-table queries, don't bother qualifying the column names to make the query easier to read if (query.FromClause.TableReferences[0] is NamedTableReference) @@ -399,7 +401,7 @@ private static void AddSelectElements(QuerySpecification query, object[] items, /// A mapping of table aliases to the logical name /// Indicates if the NOLOCK table hint should be applied /// The data source including any required joins - private static TableReference BuildJoins(IOrganizationService org, IAttributeMetadataCache metadata, TableReference dataSource, NamedTableReference parentTable, object[] items, QuerySpecification query, IDictionary aliasToLogicalName, bool archive, bool nolock, FetchXml2SqlOptions options, IDictionary ctes, IDictionary parameters, ref bool requiresTimeZone, ref bool usesToday) + private static TableReference BuildJoins(IOrganizationService org, IAttributeMetadataCache metadata, TableReference dataSource, NamedTableReference parentTable, object[] items, QuerySpecification query, IDictionary aliasToLogicalName, bool archive, bool nolock, FetchXml2SqlOptions options, IDictionary ctes, IDictionary parameters, ref bool requiresTimeZone, ref bool usesToday, ref BooleanExpression where) { if (items == null) return dataSource; @@ -407,10 +409,6 @@ private static TableReference BuildJoins(IOrganizationService org, IAttributeMet // Find any elements to process foreach (var link in items.OfType()) { - // Store the alias of this link - if (!String.IsNullOrEmpty(link.alias)) - aliasToLogicalName[link.alias] = link.name; - // Create the new table reference var table = new NamedTableReference { @@ -433,6 +431,159 @@ private static TableReference BuildJoins(IOrganizationService org, IAttributeMet if (nolock) table.TableHints.Add(new TableHint { HintKind = TableHintKind.NoLock }); + if (link.linktype == "exists" || link.linktype == "in" || link.linktype == "matchfirstrowusingcrossapply" + || link.linktype == "any" || link.linktype == "not any" || link.linktype == "not all" || link.linktype == "all") + { + // Build a whole new query for the EXISTS subquery + var subqueryFilter = GetFilter(org, metadata, link.Items, link.alias ?? link.name, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); + + var subquery = new QuerySpecification(); + + if (link.linktype == "exists" || link.linktype == "in" || link.linktype == "any" || link.linktype == "not any" + || link.linktype == "not all" || link.linktype == "all") + { + subquery.SelectElements.Add(new SelectScalarExpression + { + Expression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier{ Value = link.alias ?? link.name }, + new Identifier { Value = link.from } + } + } + } + }); + } + else if (link.linktype == "matchfirstrowusingcrossapply") + { + AddSelectElements(subquery, link.Items, link.alias ?? link.name); + AddSelectElements(query, link.Items, link.alias ?? link.name); + } + + subquery.FromClause = new FromClause + { + TableReferences = { table } + }; + + if (archive) + ((NamedTableReference)query.FromClause.TableReferences[0]).SchemaObject.Identifiers.Insert(0, new Identifier { Value = "archive" }); + + if (nolock) + ((NamedTableReference)query.FromClause.TableReferences[0]).TableHints.Add(new TableHint { HintKind = TableHintKind.NoLock }); + + // Recurse into link-entities to build joins + subquery.FromClause.TableReferences[0] = BuildJoins(org, metadata, subquery.FromClause.TableReferences[0], (NamedTableReference)subquery.FromClause.TableReferences[0], link.Items, subquery, aliasToLogicalName, archive, nolock, options, ctes, parameters, ref requiresTimeZone, ref usesToday, ref subqueryFilter); + + var correlatedFilter = new BooleanComparisonExpression + { + FirstExpression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier { Value = parentTable.Alias?.Value ?? parentTable.SchemaObject.Identifiers.Last().Value }, + new Identifier { Value = link.to } + } + } + }, + ComparisonType = BooleanComparisonType.Equals, + SecondExpression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier{ Value = link.alias ?? link.name }, + new Identifier { Value = link.from } + } + } + } + }; + + if (link.linktype == "exists" || link.linktype == "matchfirstrowusingcrossapply" || link.linktype == "any"|| + link.linktype == "not any" || link.linktype == "not all" || link.linktype == "all") + { + subqueryFilter = CombineExpressions(subqueryFilter, BooleanBinaryExpressionType.And, correlatedFilter); + } + + subquery.WhereClause = new WhereClause { SearchCondition = subqueryFilter }; + + if (link.linktype == "exists" || link.linktype == "any" || link.linktype == "not any" || link.linktype == "not all" || link.linktype == "all") + { + var existsPredicate = (BooleanExpression) new ExistsPredicate + { + Subquery = new ScalarSubquery + { + QueryExpression = subquery + } + }; + + if (link.linktype == "not any") + { + existsPredicate = new BooleanNotExpression { Expression = existsPredicate }; + } + else if (link.linktype == "all") + { + existsPredicate = new BooleanNotExpression { Expression = existsPredicate }; + var unfilteredQuery = new QuerySpecification + { + SelectElements = { subquery.SelectElements[0] }, + FromClause = subquery.FromClause, + WhereClause = new WhereClause { SearchCondition = correlatedFilter } + }; + existsPredicate = CombineExpressions(new ExistsPredicate { Subquery = new ScalarSubquery { QueryExpression = unfilteredQuery } }, BooleanBinaryExpressionType.And, existsPredicate); + } + + where = CombineExpressions(where, BooleanBinaryExpressionType.And, existsPredicate); + } + else if (link.linktype == "in") + { + var inPredicate = new InPredicate + { + Expression = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier + { + Identifiers = + { + new Identifier { Value = parentTable.Alias?.Value ?? parentTable.SchemaObject.Identifiers.Last().Value }, + new Identifier { Value = link.to } + } + } + }, + Subquery = new ScalarSubquery + { + QueryExpression = subquery + } + }; + + where = CombineExpressions(where, BooleanBinaryExpressionType.And, inPredicate); + } + else if (link.linktype == "matchfirstrowusingcrossapply") + { + subquery.TopRowFilter = new TopRowFilter { Expression = new IntegerLiteral { Value = "1" } }; + dataSource = new UnqualifiedJoin + { + FirstTableReference = dataSource, + UnqualifiedJoinType = UnqualifiedJoinType.CrossApply, + SecondTableReference = new QueryDerivedTable + { + QueryExpression = subquery, + Alias = new Identifier { Value = link.alias ?? link.name } + } + }; + } + continue; + } + + // Store the alias of this link + if (!String.IsNullOrEmpty(link.alias)) + aliasToLogicalName[link.alias] = link.name; + // Add the join from the current data source to the new table var join = new QualifiedJoin { @@ -474,34 +625,34 @@ private static TableReference BuildJoins(IOrganizationService org, IAttributeMet var filter = GetFilter(org, metadata, link.Items, link.alias ?? link.name, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); if (filter != null) - { - if (!(filter is BooleanBinaryExpression bbe) || bbe.BinaryExpressionType == BooleanBinaryExpressionType.And) - { - join.SearchCondition = new BooleanBinaryExpression - { - FirstExpression = join.SearchCondition, - BinaryExpressionType = BooleanBinaryExpressionType.And, - SecondExpression = filter - }; - } - else - { - join.SearchCondition = new BooleanBinaryExpression - { - FirstExpression = new BooleanParenthesisExpression { Expression = join.SearchCondition }, - BinaryExpressionType = BooleanBinaryExpressionType.And, - SecondExpression = new BooleanParenthesisExpression { Expression = filter } - }; - } - } + join.SearchCondition = CombineExpressions(join.SearchCondition, BooleanBinaryExpressionType.And, filter); // Recurse into any other links - dataSource = BuildJoins(org, metadata, join, (NamedTableReference)join.SecondTableReference, link.Items, query, aliasToLogicalName, archive, nolock, options, ctes, parameters, ref requiresTimeZone, ref usesToday); + dataSource = BuildJoins(org, metadata, join, (NamedTableReference)join.SecondTableReference, link.Items, query, aliasToLogicalName, archive, nolock, options, ctes, parameters, ref requiresTimeZone, ref usesToday, ref where); } return dataSource; } + private static BooleanExpression CombineExpressions(BooleanExpression expr1, BooleanBinaryExpressionType type, BooleanExpression expr2) + { + if (expr2 is BooleanBinaryExpression bbe && bbe.BinaryExpressionType != type) + expr2 = new BooleanParenthesisExpression { Expression = expr2 }; + + if (expr1 == null) + return expr2; + + if (expr1 is BooleanBinaryExpression lhs && lhs.BinaryExpressionType != type) + expr2 = new BooleanParenthesisExpression { Expression = expr1 }; + + return new BooleanBinaryExpression + { + FirstExpression = expr1, + BinaryExpressionType = type, + SecondExpression = expr2 + }; + } + /// /// Converts a FetchXML <filter> to a SQL condition /// @@ -559,58 +710,35 @@ private static BooleanExpression GetFilter(IOrganizationService org, IAttributeM /// The SQL condition equivalent of the private static BooleanExpression GetFilter(IOrganizationService org, IAttributeMetadataCache metadata, filter filter, string prefix, IDictionary aliasToLogicalName, FetchXml2SqlOptions options, IDictionary ctes, IDictionary parameters, ref bool requiresTimeZone, ref bool usesToday) { + if (filter.Items == null) + return null; + BooleanExpression expression = null; var type = filter.type == filterType.and ? BooleanBinaryExpressionType.And : BooleanBinaryExpressionType.Or; - // Convert each within the filter - foreach (var condition in filter.Items.OfType()) + foreach (var item in filter.Items) { - var newExpression = GetCondition(org, metadata, condition, prefix, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); - - if (newExpression is BooleanBinaryExpression bbe && bbe.BinaryExpressionType != type) - newExpression = new BooleanParenthesisExpression { Expression = newExpression }; - - if (expression == null) - { - expression = newExpression; - } - else + if (item is condition condition) { - if (expression is BooleanBinaryExpression lhs && lhs.BinaryExpressionType != type) - expression = new BooleanParenthesisExpression { Expression = expression }; + // Convert each within the filter + var newExpression = GetCondition(org, metadata, condition, prefix, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); - expression = new BooleanBinaryExpression - { - FirstExpression = expression, - BinaryExpressionType = type, - SecondExpression = newExpression - }; + expression = CombineExpressions(expression, type, newExpression); } - } - - // Recurse into sub-s - foreach (var subFilter in filter.Items.OfType()) - { - var newExpression = GetFilter(org, metadata, subFilter, prefix, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); - - if (newExpression is BooleanBinaryExpression bbe && bbe.BinaryExpressionType != type) - newExpression = new BooleanParenthesisExpression { Expression = newExpression }; - - if (expression == null) + else if (item is filter subFilter) { - expression = newExpression; + // Recurse into sub-s + var newExpression = GetFilter(org, metadata, subFilter, prefix, aliasToLogicalName, options, ctes, parameters, ref requiresTimeZone, ref usesToday); + + expression = CombineExpressions(expression, type, newExpression); } - else + else if (item is FetchLinkEntityType linkEntity) { - if (expression is BooleanBinaryExpression lhs && lhs.BinaryExpressionType != type) - expression = new BooleanParenthesisExpression { Expression = expression }; + // Convert related record filters in + BooleanExpression newExpression = null; + BuildJoins(org, metadata, null, new NamedTableReference { Alias = new Identifier { Value = prefix } }, new[] { item }, null, aliasToLogicalName, false, false, options, ctes, parameters, ref requiresTimeZone, ref usesToday, ref newExpression); - expression = new BooleanBinaryExpression - { - FirstExpression = expression, - BinaryExpressionType = type, - SecondExpression = newExpression - }; + expression = CombineExpressions(expression, type, newExpression); } } @@ -660,14 +788,16 @@ private static BooleanExpression GetCondition(IOrganizationService org, IAttribu object parameterValue = null; if (!String.IsNullOrEmpty(condition.ValueOf)) { + var parts = condition.ValueOf.Split('.'); + value = new ColumnReferenceExpression { MultiPartIdentifier = new MultiPartIdentifier { Identifiers = { - new Identifier{Value = condition.entityname ?? prefix}, - new Identifier{Value = condition.attribute} + new Identifier{Value = parts.Length == 2 ? parts[0] : condition.entityname ?? prefix}, + new Identifier{Value = parts.Length == 2 ? parts[1] : condition.ValueOf} } } }; @@ -2426,7 +2556,7 @@ private static int GetUserLanguageCode(IOrganizationService org) /// The name or alias of the <entity> or <link-entity> that the sorts are from /// The items within the <entity> or <link-entity> to take the sorts from /// The SQL query to apply the sorts to - private static void AddOrderBy(string name, object[] items, QuerySpecification query) + private static void AddOrderBy(string name, object[] items, QuerySpecification query, bool useRawOrderBy, IAttributeMetadataCache metadata, Dictionary aliasToLogicalName) { if (items == null) return; @@ -2447,7 +2577,7 @@ private static void AddOrderBy(string name, object[] items, QuerySpecification q { Identifiers = { - new Identifier{Value = sort.alias} + new Identifier { Value = sort.alias } } } }, @@ -2456,6 +2586,18 @@ private static void AddOrderBy(string name, object[] items, QuerySpecification q } else { + var entityAlias = sort.entityname ?? name; + var attributeName = sort.attribute; + + if (!aliasToLogicalName.TryGetValue(entityAlias, out var entityLogicalName)) + entityLogicalName = entityAlias; + + var entityMetadata = metadata[entityLogicalName]; + var attr = entityMetadata.Attributes.SingleOrDefault(a => a.LogicalName == attributeName); + + if (attr is LookupAttributeMetadata || ((attr is EnumAttributeMetadata || attr is BooleanAttributeMetadata) && !useRawOrderBy)) + attributeName += "name"; + query.OrderByClause.OrderByElements.Add(new ExpressionWithSortOrder { Expression = new ColumnReferenceExpression @@ -2463,10 +2605,10 @@ private static void AddOrderBy(string name, object[] items, QuerySpecification q MultiPartIdentifier = new MultiPartIdentifier { Identifiers = - { - new Identifier{Value = name}, - new Identifier{Value = sort.attribute} - } + { + new Identifier { Value = entityAlias }, + new Identifier { Value = attributeName } + } } }, SortOrder = sort.descending ? SortOrder.Descending : SortOrder.Ascending @@ -2476,7 +2618,7 @@ private static void AddOrderBy(string name, object[] items, QuerySpecification q // Recurse into link entities foreach (var link in items.OfType()) - AddOrderBy(link.alias ?? link.name, link.Items, query); + AddOrderBy(link.alias ?? link.name, link.Items, query, useRawOrderBy, metadata, aliasToLogicalName); } private class SimplifyMultiPartIdentifierVisitor : TSqlFragmentVisitor @@ -2495,6 +2637,10 @@ public override void ExplicitVisit(MultiPartIdentifier node) base.ExplicitVisit(node); } + + public override void ExplicitVisit(ExistsPredicate node) + { + } } private class QuoteIdentifiersVisitor : TSqlFragmentVisitor diff --git a/MarkMpn.Sql4Cds.Engine/FetchXmlExtensions.cs b/MarkMpn.Sql4Cds.Engine/FetchXmlExtensions.cs index 1961dd40..f743e71d 100644 --- a/MarkMpn.Sql4Cds.Engine/FetchXmlExtensions.cs +++ b/MarkMpn.Sql4Cds.Engine/FetchXmlExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using System.Xml.Serialization; using MarkMpn.Sql4Cds.Engine.FetchXml; namespace MarkMpn.Sql4Cds.Engine @@ -45,7 +47,7 @@ public static FetchLinkEntityType FindLinkEntity(object[] items, string alias) foreach (var linkEntity in items.OfType()) { - if (linkEntity.alias.Equals(alias, StringComparison.OrdinalIgnoreCase)) + if (linkEntity.alias != null && linkEntity.alias.Equals(alias, StringComparison.OrdinalIgnoreCase)) return linkEntity; var childMatch = FindLinkEntity(linkEntity.Items, alias); @@ -149,5 +151,32 @@ public static void RemoveAttributes(this FetchEntityType entity) link.Items = link.Items.Where(i => !(i is FetchAttributeType || i is allattributes)).ToArray(); } } + + public static FetchLinkEntityType RemoveNotNullJoinCondition(this FetchLinkEntityType linkEntity) + { + if (linkEntity.Items == null) + return linkEntity; + + foreach (var filter in linkEntity.Items.OfType()) + { + var notNull = filter.Items + .OfType() + .Where(c => c.attribute == linkEntity.from && c.entityname == null && c.@operator == @operator.notnull); + + filter.Items = filter.Items.Except(notNull).ToArray(); + } + + return linkEntity; + } + + + public static IEnumerable GetConditions(this filter filter) + { + return filter.Items + .OfType() + .Concat(filter.Items + .OfType() + .SelectMany(f => f.GetConditions())); + } } } diff --git a/MarkMpn.Sql4Cds.Engine/IQueryExecutionOptions.cs b/MarkMpn.Sql4Cds.Engine/IQueryExecutionOptions.cs index f7e097ed..507d425c 100644 --- a/MarkMpn.Sql4Cds.Engine/IQueryExecutionOptions.cs +++ b/MarkMpn.Sql4Cds.Engine/IQueryExecutionOptions.cs @@ -78,21 +78,11 @@ interface IQueryExecutionOptions /// int MaxDegreeOfParallelism { get; } - /// - /// Indicates if the server supports column comparison conditions in FetchXML - /// - bool ColumnComparisonAvailable { get; } - /// /// Indicates if date/time values should be interpreted in the local timezone or in UTC /// bool UseLocalTimeZone { get; } - /// - /// Returns a list of join operators that are supported by the server - /// - List JoinOperatorsAvailable { get; } - /// /// Indicates if plugins should be bypassed when executing DML operations /// diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.FetchXml.nuspec b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.FetchXml.nuspec index cddda3d3..7c916a6d 100644 --- a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.FetchXml.nuspec +++ b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.FetchXml.nuspec @@ -11,7 +11,7 @@ http://markcarrington.dev/sql4cds-icon/ Converts FetchXML to SQL queries. This is a minimal source-only package, you can also use the MarkMpn.Sql4Cds.Engine package that includes this conversion as well as SQL to FetchXML conversion. Minimal source-only package to convert FetchXML to SQL - Handle converting long-term retention (datasource="archive") queries to SQL + Added support for latest Fetch XML features Copyright © 2020 Mark Carrington en-GB FetchXML SQL CDS diff --git a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.nuspec b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.nuspec index df2c3120..b10a96cb 100644 --- a/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.nuspec +++ b/MarkMpn.Sql4Cds.Engine/MarkMpn.Sql4Cds.Engine.nuspec @@ -11,15 +11,25 @@ https://markcarrington.dev/sql4cds-icon/ Convert SQL queries to FetchXml and execute them against Dataverse / D365 Convert SQL queries to FetchXml and execute them against Dataverse / D365 - - Generate the execution plan for each statement in a batch only when necessary, to allow initial statements to succeed regardless of errors in later statements - Fixed escaping column names for SELECT and INSERT commands - Improved setting a partylist attribute based on an EntityReference value - Fixed sorting results for UNION - Fold DISTNCT to data source for UNION - Fold groupings without aggregates to DISTINCT - Fixed folding filters through nested loops with outer references - Fixed use of recursive CTE references within subquery + Added support for latest Fetch XML features +Support TRY, CATCH & THROW statements and related functions +Error handling consistency with SQL Server +Improved performance with large numbers of expressions and large VALUES lists +Generate the execution plan for each statement in a batch only when necessary, to allow initial statements to succeed regardless of errors in later statements +Allow access to catalog views using TDS Endpoint +Inproved EXECUTE AS support +Handle missing values in XML .value() method +Detect TDS Endpoint incompatibility with XML data type methods and error handling functions +Fixed use of TOP 1 with IN expression +Fixed escaping column names for SELECT and INSERT commands +Improved setting a partylist attribute based on an EntityReference value +Fixed sorting results for UNION +Fold DISTINCT to data source for UNION +Fold groupings without aggregates to DISTINCT +Fixed folding filters through nested loops with outer references +Fixed use of recursive CTE references within subquery +Improved performance of CROSS APPLY and OUTER APPLY +Improved query cancellation Copyright © 2020 Mark Carrington en-GB diff --git a/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs b/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs index 2b6f6a12..dbbd6ef3 100644 --- a/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs +++ b/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs @@ -36,6 +36,8 @@ class StubOptions : IQueryExecutionOptions public bool ColumnComparisonAvailable => throw new NotImplementedException(); + public bool OrderByEntityNameAvailable => throw new NotImplementedException(); + public bool UseLocalTimeZone => throw new NotImplementedException(); public List JoinOperatorsAvailable => throw new NotImplementedException(); diff --git a/MarkMpn.Sql4Cds.Engine/ScriptDom.cs b/MarkMpn.Sql4Cds.Engine/ScriptDom.cs index 74c5d224..07e8da88 100644 --- a/MarkMpn.Sql4Cds.Engine/ScriptDom.cs +++ b/MarkMpn.Sql4Cds.Engine/ScriptDom.cs @@ -3,6 +3,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Web.UI.WebControls; +using System.Xml.Linq; +using Castle.DynamicProxy.Generators.Emitters.SimpleAST; +using Microsoft.Xrm.Sdk.Query; +using Newtonsoft.Json.Linq; /// /// Basic shims for ScriptDom classes required to build a SQL statement from FetchXml @@ -17,9 +22,6 @@ class TSqlBatch public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var statement in Statements) - statement.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -48,9 +50,6 @@ class DeclareVariableStatement : Statement public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var declaration in Declarations) - declaration.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -82,9 +81,6 @@ class DeclareVariableElement public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - VariableName?.Accept(visitor); - DataType?.Accept(visitor); - Value?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -110,8 +106,6 @@ class SelectStatement : Statement public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - QueryExpression?.Accept(visitor); - WithCtesAndXmlNamespaces?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -149,16 +143,6 @@ class QuerySpecification : QueryExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - TopRowFilter?.Accept(visitor); - - foreach (var element in SelectElements) - element.Accept(visitor); - - FromClause?.Accept(visitor); - OffsetClause?.Accept(visitor); - WhereClause?.Accept(visitor); - GroupByClause?.Accept(visitor); - OrderByClause?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -216,9 +200,6 @@ class BinaryQueryExpression : QueryExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstQueryExpression?.Accept(visitor); - SecondQueryExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -258,7 +239,6 @@ class TopRowFilter public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - Expression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -374,9 +354,6 @@ class FromClause public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var table in TableReferences) - table.Accept(visitor); } public void ToString(StringBuilder buf, int indent, int longestClauseLength) @@ -418,13 +395,6 @@ class NamedTableReference : TableReference public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - SchemaObject?.Accept(visitor); - - foreach (var hint in TableHints) - hint.Accept(visitor); - - Alias?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -467,10 +437,6 @@ class QualifiedJoin : TableReference public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstTableReference?.Accept(visitor); - SecondTableReference?.Accept(visitor); - SearchCondition?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -510,9 +476,6 @@ class SchemaObjectName : MultiPartIdentifier public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var identifier in Identifiers) - identifier.Accept(visitor); } } @@ -552,9 +515,6 @@ class MultiPartIdentifier public virtual void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var identifier in Identifiers) - identifier.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -607,9 +567,6 @@ class OffsetClause public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - OffsetExpression?.Accept(visitor); - FetchExpression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent, int longestClauseLength) @@ -631,8 +588,6 @@ class WhereClause public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - SearchCondition?.Accept(visitor); } public void ToString(StringBuilder buf, int indent, int longestClauseLength) @@ -659,8 +614,6 @@ class SelectStarExpression : SelectElement public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Qualifier?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -684,9 +637,6 @@ class SelectScalarExpression : SelectElement public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); - ColumnName?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -712,8 +662,6 @@ class ColumnReferenceExpression : ScalarExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - MultiPartIdentifier?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -733,11 +681,6 @@ class FunctionCall : ScalarExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FunctionName?.Accept(visitor); - - foreach (var param in Parameters) - param.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -769,9 +712,6 @@ class ConvertCall : ScalarExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - DataType?.Accept(visitor); - Parameter?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -793,8 +733,6 @@ class SqlDataTypeReference public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Name?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -816,8 +754,6 @@ class IdentifierOrValueExpression public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Identifier?.Accept(visitor); } public void ToString(StringBuilder buf) @@ -833,9 +769,6 @@ class GroupByClause public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var group in GroupingSpecifications) - group.Accept(visitor); } public void ToString(StringBuilder buf, int indent, int longestClauseLength) @@ -866,8 +799,6 @@ class ExpressionGroupingSpecification public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -889,9 +820,6 @@ class BooleanComparisonExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstExpression?.Accept(visitor); - SecondExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -949,8 +877,6 @@ class BooleanParenthesisExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -970,9 +896,6 @@ class BooleanBinaryExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstExpression?.Accept(visitor); - SecondExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1015,9 +938,6 @@ class LikePredicate : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstExpression?.Accept(visitor); - SecondExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1043,10 +963,6 @@ class BooleanTernaryExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstExpression?.Accept(visitor); - SecondExpression?.Accept(visitor); - ThirdExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1089,13 +1005,6 @@ class InPredicate : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); - - foreach (var value in Values) - value.Accept(visitor); - - Subquery?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1137,11 +1046,6 @@ class FullTextPredicate : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var col in Columns) - col.Accept(visitor); - - Value?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1179,8 +1083,6 @@ class BooleanNotExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1201,9 +1103,6 @@ class BinaryExpression : ScalarExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - FirstExpression?.Accept(visitor); - SecondExpression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1248,8 +1147,6 @@ class UnaryExpression : ScalarExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1308,8 +1205,6 @@ class ScalarSubquery public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - QueryExpression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -1326,8 +1221,6 @@ class BooleanIsNullExpression : BooleanExpression public override void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public override void ToString(StringBuilder buf, int indent) @@ -1348,9 +1241,6 @@ class OrderByClause public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var order in OrderByElements) - order.Accept(visitor); } public void ToString(StringBuilder buf, int indent, int longestClauseLength) @@ -1383,8 +1273,6 @@ class ExpressionWithSortOrder public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - Expression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -1414,9 +1302,6 @@ class WithCtesAndXmlNamespaces public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - foreach (var cte in CommonTableExpressions) - cte.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -1450,13 +1335,6 @@ class CommonTableExpression public void Accept(TSqlFragmentVisitor visitor) { visitor.ExplicitVisit(this); - - ExpressionName?.Accept(visitor); - - foreach (var col in Columns) - col.Accept(visitor); - - QueryExpression?.Accept(visitor); } public void ToString(StringBuilder buf, int indent) @@ -1478,208 +1356,383 @@ public void ToString(StringBuilder buf, int indent) } } + class ExistsPredicate : BooleanExpression + { + public ScalarSubquery Subquery { get; set; } + + public override void Accept(TSqlFragmentVisitor visitor) + { + visitor.ExplicitVisit(this); + } + + public override void ToString(StringBuilder buf, int indent) + { + buf.Append("EXISTS("); + Subquery?.ToString(buf, indent); + buf.Append(")"); + } + } + + class QueryDerivedTable : TableReference + { + public QueryExpression QueryExpression { get; set; } + + public Identifier Alias { get; set; } + + public override void Accept(TSqlFragmentVisitor visitor) + { + visitor.ExplicitVisit(this); + } + + public override void ToString(StringBuilder buf, int indent) + { + buf.Append("("); + QueryExpression?.ToString(buf, indent); + buf.Append(")"); + + if (Alias != null) + { + buf.Append(" AS "); + Alias.ToString(buf); + } + } + } + + class UnqualifiedJoin : TableReference + { + public TableReference FirstTableReference { get; set; } + public UnqualifiedJoinType UnqualifiedJoinType { get; set; } + public TableReference SecondTableReference { get; set; } + + public override void Accept(TSqlFragmentVisitor visitor) + { + visitor.ExplicitVisit(this); + } + + public override void ToString(StringBuilder buf, int indent) + { + FirstTableReference?.ToString(buf, indent); + buf.Append(" CROSS APPLY "); + SecondTableReference?.ToString(buf, indent); + } + } + enum SortOrder { Ascending, Descending } + enum UnqualifiedJoinType + { + CrossApply + } + class TSqlFragmentVisitor { public virtual void ExplicitVisit(TSqlBatch node) { + foreach (var statement in node.Statements) + statement.Accept(this); } public virtual void ExplicitVisit(DeclareVariableStatement node) { + foreach (var declaration in node.Declarations) + declaration.Accept(this); } public virtual void ExplicitVisit(DeclareVariableElement node) { + node.VariableName?.Accept(this); + node.DataType?.Accept(this); + node.Value?.Accept(this); } public virtual void ExplicitVisit(SelectStatement node) { + node.QueryExpression?.Accept(this); + node.WithCtesAndXmlNamespaces?.Accept(this); } public virtual void ExplicitVisit(MultiPartIdentifier node) { + foreach (var identifier in node.Identifiers) + identifier.Accept(this); } public virtual void ExplicitVisit(Identifier node) { } - public virtual void ExplicitVisit(QuerySpecification querySpecification) + public virtual void ExplicitVisit(QuerySpecification node) + { + node.TopRowFilter?.Accept(this); + + foreach (var element in node.SelectElements) + element.Accept(this); + + node.FromClause?.Accept(this); + node.OffsetClause?.Accept(this); + node.WhereClause?.Accept(this); + node.GroupByClause?.Accept(this); + node.OrderByClause?.Accept(this); + } + + public virtual void ExplicitVisit(TopRowFilter node) + { + node.Expression?.Accept(this); + } + + public virtual void ExplicitVisit(NullLiteral node) { } - public virtual void ExplicitVisit(TopRowFilter topRowFilter) + public virtual void ExplicitVisit(IntegerLiteral node) { } - public virtual void ExplicitVisit(NullLiteral integerLiteral) + public virtual void ExplicitVisit(StringLiteral node) { } - public virtual void ExplicitVisit(IntegerLiteral integerLiteral) + public virtual void ExplicitVisit(BinaryLiteral node) { } - public virtual void ExplicitVisit(StringLiteral stringLiteral) + public virtual void ExplicitVisit(NumericLiteral node) { } - public virtual void ExplicitVisit(BinaryLiteral binaryLiteral) + public virtual void ExplicitVisit(MoneyLiteral node) { } - public virtual void ExplicitVisit(NumericLiteral numericLiteral) + public virtual void ExplicitVisit(FromClause node) { + foreach (var table in node.TableReferences) + table.Accept(this); } - public virtual void ExplicitVisit(MoneyLiteral moneyLiteral) + public virtual void ExplicitVisit(NamedTableReference node) { + node.SchemaObject?.Accept(this); + + foreach (var hint in node.TableHints) + hint.Accept(this); + + node.Alias?.Accept(this); } - public virtual void ExplicitVisit(FromClause fromClause) + public virtual void ExplicitVisit(QualifiedJoin node) { + node.FirstTableReference?.Accept(this); + node.SecondTableReference?.Accept(this); + node.SearchCondition?.Accept(this); } - public virtual void ExplicitVisit(NamedTableReference namedTableReference) + public virtual void ExplicitVisit(SchemaObjectName node) { + foreach (var identifier in node.Identifiers) + identifier.Accept(this); } - public virtual void ExplicitVisit(QualifiedJoin qualifiedJoin) + public virtual void ExplicitVisit(OffsetClause node) { + node.OffsetExpression?.Accept(this); + node.FetchExpression?.Accept(this); } - public virtual void ExplicitVisit(SchemaObjectName schemaObjectName) + public virtual void ExplicitVisit(WhereClause node) { + node.SearchCondition?.Accept(this); } - public virtual void ExplicitVisit(OffsetClause offsetClause) + public virtual void ExplicitVisit(SelectStarExpression node) { + node.Qualifier?.Accept(this); } - public virtual void ExplicitVisit(WhereClause whereClause) + public virtual void ExplicitVisit(SelectScalarExpression node) { + node.Expression?.Accept(this); + node.ColumnName?.Accept(this); } - public virtual void ExplicitVisit(SelectStarExpression selectStarExpression) + public virtual void ExplicitVisit(ColumnReferenceExpression node) { + node.MultiPartIdentifier?.Accept(this); } - public virtual void ExplicitVisit(SelectScalarExpression selectScalarExpression) + public virtual void ExplicitVisit(FunctionCall node) { + node.FunctionName?.Accept(this); + + foreach (var param in node.Parameters) + param.Accept(this); } - public virtual void ExplicitVisit(ColumnReferenceExpression columnReferenceExpression) + public virtual void ExplicitVisit(IdentifierOrValueExpression node) { + node.Identifier?.Accept(this); } - public virtual void ExplicitVisit(FunctionCall functionCall) + public virtual void ExplicitVisit(GroupByClause node) { + foreach (var group in node.GroupingSpecifications) + group.Accept(this); } - public virtual void ExplicitVisit(IdentifierOrValueExpression identifierOrValueExpression) + public virtual void ExplicitVisit(ExpressionGroupingSpecification node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(GroupByClause groupByClause) + public virtual void ExplicitVisit(BooleanComparisonExpression node) { + node.FirstExpression?.Accept(this); + node.SecondExpression?.Accept(this); } - public virtual void ExplicitVisit(ExpressionGroupingSpecification expressionGroupingSpecification) + public virtual void ExplicitVisit(BooleanBinaryExpression node) { + node.FirstExpression?.Accept(this); + node.SecondExpression?.Accept(this); } - public virtual void ExplicitVisit(BooleanComparisonExpression booleanComparisonExpression) + public virtual void ExplicitVisit(LikePredicate node) { + node.FirstExpression?.Accept(this); + node.SecondExpression?.Accept(this); } - public virtual void ExplicitVisit(BooleanBinaryExpression booleanBinaryExpression) + public virtual void ExplicitVisit(BooleanTernaryExpression node) { + node.FirstExpression?.Accept(this); + node.SecondExpression?.Accept(this); + node.ThirdExpression?.Accept(this); } - public virtual void ExplicitVisit(LikePredicate likePredicate) + public virtual void ExplicitVisit(InPredicate node) { + node.Expression?.Accept(this); + + foreach (var value in node.Values) + value.Accept(this); + + node.Subquery?.Accept(this); } - public virtual void ExplicitVisit(BooleanTernaryExpression booleanTernaryExpression) + public virtual void ExplicitVisit(BooleanIsNullExpression node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(InPredicate inPredicate) + public virtual void ExplicitVisit(OrderByClause node) { + foreach (var order in node.OrderByElements) + order.Accept(this); } - public virtual void ExplicitVisit(BooleanIsNullExpression booleanIsNullExpression) + public virtual void ExplicitVisit(ExpressionWithSortOrder node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(OrderByClause orderByClause) + public virtual void ExplicitVisit(TableHint node) { } - public virtual void ExplicitVisit(ExpressionWithSortOrder expressionWithSortOrder) + public virtual void ExplicitVisit(ConvertCall node) { + node.DataType?.Accept(this); + node.Parameter?.Accept(this); } - public virtual void ExplicitVisit(TableHint tableHint) + public virtual void ExplicitVisit(SqlDataTypeReference node) { + node.Name?.Accept(this); } - public virtual void ExplicitVisit(ConvertCall convertCall) + public virtual void ExplicitVisit(VariableReference node) { } - public virtual void ExplicitVisit(SqlDataTypeReference sqlDataTypeReference) + public virtual void ExplicitVisit(BooleanParenthesisExpression node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(VariableReference variableReference) + public virtual void ExplicitVisit(WithCtesAndXmlNamespaces node) { + foreach (var cte in node.CommonTableExpressions) + cte.Accept(this); } - public virtual void ExplicitVisit(BooleanParenthesisExpression booleanParenthesisExpression) + public virtual void ExplicitVisit(CommonTableExpression node) { + node.ExpressionName?.Accept(this); + + foreach (var col in node.Columns) + col.Accept(this); + + node.QueryExpression?.Accept(this); } - public virtual void ExplicitVisit(WithCtesAndXmlNamespaces withCtesAndXmlNamespaces) + public virtual void ExplicitVisit(ScalarSubquery node) { + node.QueryExpression?.Accept(this); } - public virtual void ExplicitVisit(CommonTableExpression commonTableExpression) + public virtual void ExplicitVisit(BinaryQueryExpression node) { + node.FirstQueryExpression?.Accept(this); + node.SecondQueryExpression?.Accept(this); } - public virtual void ExplicitVisit(ScalarSubquery scalarSubquery) + public virtual void ExplicitVisit(BinaryExpression node) { + node.FirstExpression?.Accept(this); + node.SecondExpression?.Accept(this); } - public virtual void ExplicitVisit(BinaryQueryExpression binaryQueryExpression) + public virtual void ExplicitVisit(UnaryExpression node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(BinaryExpression binaryExpression) + public virtual void ExplicitVisit(ParameterlessCall node) { } - public virtual void ExplicitVisit(UnaryExpression unaryExpression) + public virtual void ExplicitVisit(FullTextPredicate node) + { + foreach (var col in node.Columns) + col.Accept(this); + + node.Value?.Accept(this); + } + + public virtual void ExplicitVisit(BooleanNotExpression node) { + node.Expression?.Accept(this); } - public virtual void ExplicitVisit(ParameterlessCall unaryExpression) + public virtual void ExplicitVisit(ExistsPredicate node) { + node.Subquery?.Accept(this); } - public virtual void ExplicitVisit(FullTextPredicate fullTextPredicate) + public virtual void ExplicitVisit(QueryDerivedTable node) { + node.QueryExpression?.Accept(this); + node.Alias?.Accept(this); } - public virtual void ExplicitVisit(BooleanNotExpression notExpression) + public virtual void ExplicitVisit(UnqualifiedJoin node) { + node.FirstTableReference?.Accept(this); + node.SecondTableReference?.Accept(this); } } diff --git a/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs index fb3d8287..36cb1110 100644 --- a/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs +++ b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs @@ -61,9 +61,27 @@ public TDSEndpointCompatibilityVisitor(IDbConnection con, IAttributeMetadataCach using (var reader = cmd.ExecuteReader()) { while (reader.Read()) - _supportedTables.Add(reader.GetString(0)); + _supportedTables.Add("dbo." + reader.GetString(0)); } } + + _supportedTables.Add("sys.all_columns"); + _supportedTables.Add("sys.all_objects"); + _supportedTables.Add("sys.check_constraints"); + _supportedTables.Add("sys.columns"); + _supportedTables.Add("sys.computed_columns"); + _supportedTables.Add("sys.default_constraints"); + _supportedTables.Add("sys.foreign_key_columns"); + _supportedTables.Add("sys.foreign_keys"); + _supportedTables.Add("sys.index_columns"); + _supportedTables.Add("sys.objects"); + _supportedTables.Add("sys.sequences"); + _supportedTables.Add("sys.sql_modules"); + _supportedTables.Add("sys.stats"); + _supportedTables.Add("sys.synonyms"); + _supportedTables.Add("sys.table_types"); + _supportedTables.Add("sys.tables"); + _supportedTables.Add("sys.triggers"); } IsCompatible = true; @@ -89,8 +107,7 @@ public override void Visit(TSqlFragment node) public override void Visit(NamedTableReference node) { if (node.SchemaObject.ServerIdentifier != null || - node.SchemaObject.DatabaseIdentifier != null || - (!String.IsNullOrEmpty(node.SchemaObject.SchemaIdentifier?.Value) && !node.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase))) + node.SchemaObject.DatabaseIdentifier != null) { // Can't do cross-instance queries // No access to metadata schema @@ -98,7 +115,7 @@ public override void Visit(NamedTableReference node) return; } - if (_supportedTables != null && !_supportedTables.Contains(node.SchemaObject.BaseIdentifier.Value) && + if (_supportedTables != null && !_supportedTables.Contains((node.SchemaObject.SchemaIdentifier?.Value ?? "dbo") + "." + node.SchemaObject.BaseIdentifier.Value) && (node.SchemaObject.Identifiers.Count != 1 || !_ctes.ContainsKey(node.SchemaObject.BaseIdentifier.Value))) { // Table does not exist in TDS endpoint and is not defined as a CTE @@ -387,16 +404,28 @@ public override void Visit(WaitForStatement node) public override void Visit(FunctionCall node) { - // Can't use JSON functions switch (node.FunctionName.Value.ToUpperInvariant()) { + // Can't use JSON functions case "JSON_VALUE": case "JSON_PATH_EXISTS": case "SQL_VARIANT_PROPERTY": + + // Can't use error handling functions + case "ERROR_LINE": + case "ERROR_MESSAGE": + case "ERROR_NUMBER": + case "ERROR_PROCEDURE": + case "ERROR_SEVERITY": + case "ERROR_STATE": IsCompatible = false; break; } + // Can't use XML data type methods + if (node.CallTarget != null) + IsCompatible = false; + base.Visit(node); } diff --git a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj index 4dd84f5e..095b3aae 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj +++ b/MarkMpn.Sql4Cds.LanguageServer/MarkMpn.Sql4Cds.LanguageServer.csproj @@ -12,7 +12,7 @@ - + diff --git a/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj b/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj index 62d0920b..088020d6 100644 --- a/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj +++ b/MarkMpn.Sql4Cds.SSMS.18/MarkMpn.Sql4Cds.SSMS.18.csproj @@ -192,7 +192,7 @@ all - 6.29.0 + 6.34.0 4.3.1 diff --git a/MarkMpn.Sql4Cds.SSMS.20.Setup/HeatGeneratedFileList.wxs b/MarkMpn.Sql4Cds.SSMS.20.Setup/HeatGeneratedFileList.wxs new file mode 100644 index 00000000..e707457b --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20.Setup/HeatGeneratedFileList.wxs @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS.20.Setup/MarkMpn.Sql4Cds.SSMS.20.Setup.wixproj b/MarkMpn.Sql4Cds.SSMS.20.Setup/MarkMpn.Sql4Cds.SSMS.20.Setup.wixproj new file mode 100644 index 00000000..183955fc --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20.Setup/MarkMpn.Sql4Cds.SSMS.20.Setup.wixproj @@ -0,0 +1,54 @@ + + + + Debug + x86 + 3.10 + {fe9ef004-bd77-49da-85b5-d51daeb76274} + 2.0 + MarkMpn.Sql4Cds.SSMS.20.Setup + Package + + + bin\$(Configuration)\ + obj\$(Configuration)\ + HarvestPath=..\MarkMpn.Sql4Cds.SSMS.20\bin\$(Configuration) + True + + + bin\$(Configuration)\ + obj\$(Configuration)\ + True + + + HarvestPath=..\MarkMpn.Sql4Cds.SSMS.19\bin\$(Configuration) + + + + + + + + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + + + + + + + + + + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS.20.Setup/Product.wxs b/MarkMpn.Sql4Cds.SSMS.20.Setup/Product.wxs new file mode 100644 index 00000000..5a328d10 --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20.Setup/Product.wxs @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.SSMS.20/Key.snk b/MarkMpn.Sql4Cds.SSMS.20/Key.snk new file mode 100644 index 00000000..f9c4d386 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/Key.snk differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj b/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj new file mode 100644 index 00000000..3d4892b8 --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/MarkMpn.Sql4Cds.SSMS.20.csproj @@ -0,0 +1,210 @@ + + + + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + true + + + + true + + + Key.snk + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445} + Library + Properties + MarkMpn.Sql4Cds.SSMS.19 + MarkMpn.Sql4Cds.SSMS + v4.7.2 + true + true + true + true + true + false + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + win + + + true + full + false + bin\Debug\ + TRACE;DEBUG;Microsoft_Data_SqlClient + prompt + 4 + True + C:\Program Files %28x86%29\Microsoft SQL Server Management Studio 20\Common7\IDE\Extensions\MarkMpn.Sql4Cds.SSMS + False + + + pdbonly + true + bin\Release\ + TRACE;Microsoft_Data_SqlClient + prompt + 4 + + + + + + + + + Designer + + + + + Menus.ctmenu + + + + + False + False + + + False + False + + + False + False + + + False + False + + + + False + References\Microsoft.SqlServer.ConnectionInfo.dll + + + False + References\Microsoft.SqlServer.RegSvrEnum.dll + + + False + False + + + + References\SQLEditors.dll + + + References\SqlPackageBase.dll + + + References\SqlWorkbench.Interfaces.dll + + + False + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + VSPackage + Designer + + + + + {04c2d073-de54-4628-b876-5965d0b75b6e} + MarkMpn.Sql4Cds.Controls + + + {23288bb2-0d6f-4329-9a5c-4c659567a652} + MarkMpn.Sql4Cds.Engine.NetFx + + + + + 9.1.1.32 + + + 3.1.1 + + + 14.3.25408 + + + 7.10.6071 + + + 15.0.10 + + + 15.0.26201 + + + 14.3.25407 + + + 17.4.2119 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 4.3.1 + + + + + + + + + + + + + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS.20/Properties/AssemblyInfo.cs b/MarkMpn.Sql4Cds.SSMS.20/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..413fe5db --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MarkMpn.Sql4Cds.SSMS")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Mark Carrington")] +[assembly: AssemblyProduct("MarkMpn.Sql4Cds.SSMS")] +[assembly: AssemblyCopyright("Copyright © 2021 - 2023 Mark Carrington")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] diff --git a/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.ConnectionInfo.dll b/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.ConnectionInfo.dll new file mode 100644 index 00000000..58b3fbcc Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.ConnectionInfo.dll differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.RegSvrEnum.dll b/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.RegSvrEnum.dll new file mode 100644 index 00000000..99ae50aa Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/References/Microsoft.SqlServer.RegSvrEnum.dll differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/References/SQLEditors.dll b/MarkMpn.Sql4Cds.SSMS.20/References/SQLEditors.dll new file mode 100644 index 00000000..8b526364 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/References/SQLEditors.dll differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/References/SqlPackageBase.dll b/MarkMpn.Sql4Cds.SSMS.20/References/SqlPackageBase.dll new file mode 100644 index 00000000..bc1d86c0 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/References/SqlPackageBase.dll differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/References/SqlWorkbench.Interfaces.dll b/MarkMpn.Sql4Cds.SSMS.20/References/SqlWorkbench.Interfaces.dll new file mode 100644 index 00000000..7f88ec40 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/References/SqlWorkbench.Interfaces.dll differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/Resources/FetchXml2SqlCommand.png b/MarkMpn.Sql4Cds.SSMS.20/Resources/FetchXml2SqlCommand.png new file mode 100644 index 00000000..d12d0ac5 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/Resources/FetchXml2SqlCommand.png differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommand.png b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommand.png new file mode 100644 index 00000000..241c9feb Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommand.png differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommandPackage.ico b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommandPackage.ico new file mode 100644 index 00000000..d323b07f Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2FetchXmlCommandPackage.ico differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2MCommand.png b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2MCommand.png new file mode 100644 index 00000000..a3393927 Binary files /dev/null and b/MarkMpn.Sql4Cds.SSMS.20/Resources/Sql2MCommand.png differ diff --git a/MarkMpn.Sql4Cds.SSMS.20/Sql4CdsPackage.vsct b/MarkMpn.Sql4Cds.SSMS.20/Sql4CdsPackage.vsct new file mode 100644 index 00000000..43d4982e --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/Sql4CdsPackage.vsct @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + DefaultDocked + + SQL 4 CDS + SQL 4 CDS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.SSMS.20/VSPackage.resx b/MarkMpn.Sql4Cds.SSMS.20/VSPackage.resx new file mode 100644 index 00000000..92e44001 --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/VSPackage.resx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Sql2FetchXmlCommand Extension + + + Sql2FetchXmlCommand Visual Studio Extension Detailed Info + + + Resources\Sql2FetchXmlCommandPackage.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS.20/app.config b/MarkMpn.Sql4Cds.SSMS.20/app.config new file mode 100644 index 00000000..f670c2e2 --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.SSMS.20/source.extension.vsixmanifest b/MarkMpn.Sql4Cds.SSMS.20/source.extension.vsixmanifest new file mode 100644 index 00000000..ab192457 --- /dev/null +++ b/MarkMpn.Sql4Cds.SSMS.20/source.extension.vsixmanifest @@ -0,0 +1,21 @@ + + + + + MarkMpn.Sql4Cds.SSMS + Empty VSIX Project. + + + + + + + + + + + + + + + diff --git a/MarkMpn.Sql4Cds.SSMS/CommandBase.cs b/MarkMpn.Sql4Cds.SSMS/CommandBase.cs index 0d18b68f..747c7271 100644 --- a/MarkMpn.Sql4Cds.SSMS/CommandBase.cs +++ b/MarkMpn.Sql4Cds.SSMS/CommandBase.cs @@ -13,6 +13,7 @@ using Microsoft.SqlServer.Management.UI.VSIntegration; using Microsoft.VisualStudio.Shell; using Microsoft.Xrm.Tooling.Connector; +using MarkMpn.Sql4Cds.Engine.ExecutionPlan; namespace MarkMpn.Sql4Cds.SSMS { @@ -20,6 +21,9 @@ internal abstract class CommandBase { private static readonly IDictionary _clientCache = new Dictionary(); private static readonly IDictionary _metadataCache = new Dictionary(); + private static readonly IDictionary _tableSizeCache = new Dictionary(); + private static readonly IDictionary _messageCache = new Dictionary(); + protected static readonly TelemetryClient _ai; static CommandBase() @@ -138,14 +142,16 @@ protected DataSource GetDataSource() var name = conStr.DataSource.Split('.')[0]; var con = ConnectCDS(conStr); var metadata = GetMetadataCache(conStr); + var tableSizeCache = GetTableSizeCache(conStr, metadata); + var messageCache = GetMessageCache(conStr, metadata); return new DataSource { Connection = con, Metadata = metadata, Name = name, - TableSizeCache = new TableSizeCache(con, metadata), - MessageCache = new MessageCache(con, metadata) + TableSizeCache = tableSizeCache, + MessageCache = messageCache }; } @@ -230,5 +236,37 @@ protected AttributeMetadataCache GetMetadataCache(SqlConnectionStringBuilder con return metadata; } + + private ITableSizeCache GetTableSizeCache(SqlConnectionStringBuilder conStr, IAttributeMetadataCache metadata) + { + if (conStr == null) + return null; + + var server = conStr.DataSource.Split(',')[0]; + + if (_tableSizeCache.TryGetValue(server, out var tableSizeCache)) + return tableSizeCache; + + tableSizeCache = new TableSizeCache(ConnectCDS(conStr), metadata); + _tableSizeCache[server] = tableSizeCache; + + return tableSizeCache; + } + + private IMessageCache GetMessageCache(SqlConnectionStringBuilder conStr, IAttributeMetadataCache metadata) + { + if (conStr == null) + return null; + + var server = conStr.DataSource.Split(',')[0]; + + if (_messageCache.TryGetValue(server, out var messageCache)) + return messageCache; + + messageCache = new MessageCache(ConnectCDS(conStr), metadata); + _messageCache[server] = messageCache; + + return messageCache; + } } } diff --git a/MarkMpn.Sql4Cds.XTB/ConnectionPropertiesWrapper.cs b/MarkMpn.Sql4Cds.XTB/ConnectionPropertiesWrapper.cs index 96940451..d5bb2c06 100644 --- a/MarkMpn.Sql4Cds.XTB/ConnectionPropertiesWrapper.cs +++ b/MarkMpn.Sql4Cds.XTB/ConnectionPropertiesWrapper.cs @@ -15,22 +15,22 @@ public ConnectionPropertiesWrapper(ConnectionDetail connection) [Category("Connection")] [DisplayName("Connection Name")] - public string ConnectionName => _connection.ConnectionName; + public string ConnectionName => _connection?.ConnectionName; [Category("Connection")] [DisplayName("Connection Id")] - public Guid? ConnectionId => _connection.ConnectionId; + public Guid? ConnectionId => _connection?.ConnectionId; [Category("Organization")] [DisplayName("Organization Name")] - public string Organization => _connection.Organization; + public string Organization => _connection?.Organization; [Category("Organization")] [DisplayName("Version")] - public string OrganizationVersion => _connection.OrganizationVersion; + public string OrganizationVersion => _connection?.OrganizationVersion; [Category("Organization")] [DisplayName("URL")] - public string OrganizationServiceUrl => _connection.OrganizationServiceUrl; + public string OrganizationServiceUrl => _connection?.OrganizationServiceUrl; } } \ No newline at end of file diff --git a/MarkMpn.Sql4Cds.XTB/PluginControl.cs b/MarkMpn.Sql4Cds.XTB/PluginControl.cs index c9528f25..ea185727 100644 --- a/MarkMpn.Sql4Cds.XTB/PluginControl.cs +++ b/MarkMpn.Sql4Cds.XTB/PluginControl.cs @@ -111,16 +111,10 @@ private void AddConnection(ConnectionDetail con) if (!_dataSources.ContainsKey(con.ConnectionName)) { var metadata = new SharedMetadataCache(con, GetNewServiceClient(con)); + var tableSize = new TableSizeCache(GetNewServiceClient(con), metadata); + var messages = new MessageCache(GetNewServiceClient(con), metadata); - _dataSources[con.ConnectionName] = new XtbDataSource - { - ConnectionDetail = con, - Connection = GetNewServiceClient(con), - Metadata = metadata, - TableSizeCache = new TableSizeCache(GetNewServiceClient(con), metadata), - Name = con.ConnectionName, - MessageCache = new MessageCache(GetNewServiceClient(con), metadata) - }; + _dataSources[con.ConnectionName] = new XtbDataSource(con, GetNewServiceClient, metadata, tableSize, messages); } // Start loading the entity list in the background diff --git a/MarkMpn.Sql4Cds.XTB/XtbDataSource.cs b/MarkMpn.Sql4Cds.XTB/XtbDataSource.cs index 1c3c1e2c..806ed3a1 100644 --- a/MarkMpn.Sql4Cds.XTB/XtbDataSource.cs +++ b/MarkMpn.Sql4Cds.XTB/XtbDataSource.cs @@ -1,15 +1,24 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using MarkMpn.Sql4Cds.Engine; +using MarkMpn.Sql4Cds.Engine.ExecutionPlan; using McTools.Xrm.Connection; +using Microsoft.Xrm.Sdk; namespace MarkMpn.Sql4Cds.XTB { class XtbDataSource : DataSource { + public XtbDataSource(ConnectionDetail connection, Func connect, IAttributeMetadataCache metadata, ITableSizeCache tableSize, IMessageCache messages) : base(connect(connection), metadata, tableSize, messages) + { + ConnectionDetail = connection; + Name = connection.ConnectionName; + } + public ConnectionDetail ConnectionDetail { get; set; } } } diff --git a/MarkMpn.Sql4Cds.sln b/MarkMpn.Sql4Cds.sln index 064515ea..575e0e24 100644 --- a/MarkMpn.Sql4Cds.sln +++ b/MarkMpn.Sql4Cds.sln @@ -44,6 +44,10 @@ Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "MarkMpn.Sql4Cds.SSMS.19.Set EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.XTB", "MarkMpn.Sql4Cds.XTB\MarkMpn.Sql4Cds.XTB.csproj", "{8050B824-A28B-4631-8A95-D127859A9216}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkMpn.Sql4Cds.SSMS.20", "MarkMpn.Sql4Cds.SSMS.20\MarkMpn.Sql4Cds.SSMS.20.csproj", "{BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "MarkMpn.Sql4Cds.SSMS.20.Setup", "MarkMpn.Sql4Cds.SSMS.20.Setup\MarkMpn.Sql4Cds.SSMS.20.Setup.wixproj", "{FE9EF004-BD77-49DA-85B5-D51DAEB76274}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -206,6 +210,28 @@ Global {8050B824-A28B-4631-8A95-D127859A9216}.Release|arm64.Build.0 = Release|Any CPU {8050B824-A28B-4631-8A95-D127859A9216}.Release|x86.ActiveCfg = Release|Any CPU {8050B824-A28B-4631-8A95-D127859A9216}.Release|x86.Build.0 = Release|Any CPU + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Debug|arm64.ActiveCfg = Debug|arm64 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Debug|arm64.Build.0 = Debug|arm64 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Debug|x86.ActiveCfg = Debug|x86 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Debug|x86.Build.0 = Debug|x86 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|Any CPU.Build.0 = Release|Any CPU + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|arm64.ActiveCfg = Release|arm64 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|arm64.Build.0 = Release|arm64 + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|x86.ActiveCfg = Release|Any CPU + {BAF95EC5-ADDF-422A-A40C-CB8F5DA1C445}.Release|x86.Build.0 = Release|Any CPU + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Debug|Any CPU.ActiveCfg = Debug|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Debug|arm64.ActiveCfg = Debug|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Debug|arm64.Build.0 = Debug|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Debug|x86.ActiveCfg = Debug|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Debug|x86.Build.0 = Debug|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|Any CPU.ActiveCfg = Release|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|Any CPU.Build.0 = Release|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|arm64.ActiveCfg = Release|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|arm64.Build.0 = Release|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|x86.ActiveCfg = Release|x86 + {FE9EF004-BD77-49DA-85B5-D51DAEB76274}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -221,6 +247,7 @@ Global MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{73283a07-671d-4ba2-b922-98987f85330b}*SharedItemsImports = 13 MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{8c71690c-4ef8-4ebf-88c0-33952fed3acf}*SharedItemsImports = 4 MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.projitems*{a570cbca-d09c-40d5-a318-a95c8fbbd593}*SharedItemsImports = 5 + MarkMpn.Sql4Cds.SSMS\MarkMpn.Sql4Cds.SSMS.projitems*{baf95ec5-addf-422a-a40c-cb8f5da1c445}*SharedItemsImports = 4 MarkMpn.Sql4Cds.Engine\MarkMpn.Sql4Cds.Engine.projitems*{bcf89341-e1a6-4106-9c12-1e15c9e7c58b}*SharedItemsImports = 13 EndGlobalSection EndGlobal diff --git a/MarkMpn.Sql4Cds/MarkMpn.SQL4CDS.nuspec b/MarkMpn.Sql4Cds/MarkMpn.SQL4CDS.nuspec index a9f422fb..f2d0c2ca 100644 --- a/MarkMpn.Sql4Cds/MarkMpn.SQL4CDS.nuspec +++ b/MarkMpn.Sql4Cds/MarkMpn.SQL4CDS.nuspec @@ -23,14 +23,25 @@ plugins or integrations by writing familiar SQL and converting it. Queries can also run using the preview TDS Endpoint. A wide range of SQL functionality is also built in to allow running queries that aren't directly supported by either FetchXML or the TDS Endpoint. Convert SQL queries to FetchXML and execute them against Dataverse / D365 - Generate the execution plan for each statement in a batch only when necessary, to allow initial statements to succeed regardless of errors in later statements + Added support for latest Fetch XML features +Support TRY, CATCH & THROW statements and related functions +Error handling consistency with SQL Server +Improved performance with large numbers of expressions and large VALUES lists +Generate the execution plan for each statement in a batch only when necessary, to allow initial statements to succeed regardless of errors in later statements +Allow access to catalog views using TDS Endpoint +Inproved EXECUTE AS support +Handle missing values in XML .value() method +Detect TDS Endpoint incompatibility with XML data type methods and error handling functions +Fixed use of TOP 1 with IN expression Fixed escaping column names for SELECT and INSERT commands Improved setting a partylist attribute based on an EntityReference value Fixed sorting results for UNION -Fold DISTNCT to data source for UNION +Fold DISTINCT to data source for UNION Fold groupings without aggregates to DISTINCT Fixed folding filters through nested loops with outer references Fixed use of recursive CTE references within subquery +Improved performance of CROSS APPLY and OUTER APPLY +Improved query cancellation Fixed opening the tool without a connection Improved copying results from grid diff --git a/build.yml b/build.yml index 6c0c1124..2082e1d2 100644 --- a/build.yml +++ b/build.yml @@ -103,6 +103,7 @@ steps: $cert.Import("$(CodeSigningCert.secureFilePath)", "$(CodeSigningCertPassword)", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::DefaultKeySet) Set-AuthenticodeSignature -Certificate $cert -FilePath "MarkMpn.Sql4Cds.SSMS.18.Setup\bin\$(buildConfiguration)\MarkMpn.Sql4Cds.SSMS.18.Setup.msi" -TimestampServer "http://timestamp.digicert.com" Set-AuthenticodeSignature -Certificate $cert -FilePath "MarkMpn.Sql4Cds.SSMS.19.Setup\bin\$(buildConfiguration)\MarkMpn.Sql4Cds.SSMS.19.Setup.msi" -TimestampServer "http://timestamp.digicert.com" + Set-AuthenticodeSignature -Certificate $cert -FilePath "MarkMpn.Sql4Cds.SSMS.19.Setup\bin\$(buildConfiguration)\MarkMpn.Sql4Cds.SSMS.20.Setup.msi" -TimestampServer "http://timestamp.digicert.com" - task: PublishPipelineArtifact@1 displayName: Publish SSMS 18 installer to pipeline @@ -117,6 +118,13 @@ steps: targetPath: 'MarkMpn.Sql4Cds.SSMS.19.Setup\bin\$(buildConfiguration)\MarkMpn.Sql4Cds.SSMS.19.Setup.msi' artifact: 'SSMS19Installer' publishLocation: 'pipeline' + +- task: PublishPipelineArtifact@1 + displayName: Publish SSMS 20 installer to pipeline + inputs: + targetPath: 'MarkMpn.Sql4Cds.SSMS.19.Setup\bin\$(buildConfiguration)\MarkMpn.Sql4Cds.SSMS.20.Setup.msi' + artifact: 'SSMS20Installer' + publishLocation: 'pipeline' - task: PublishPipelineArtifact@1 displayName: Publish version file to pipeline