diff --git a/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs b/MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs index f71493e1..1cb3c473 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; @@ -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); } } @@ -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(_localDataSource)) + { + 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(_localDataSource)) + { + 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/MarkMpn.Sql4Cds.Engine.Tests.csproj b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj index d9a2ecc1..0d4f8b18 100644 --- a/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj +++ b/MarkMpn.Sql4Cds.Engine.Tests/MarkMpn.Sql4Cds.Engine.Tests.csproj @@ -120,6 +120,9 @@ + + 2.1.28 + 1.58.1 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/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/Visitors/TDSEndpointCompatibilityVisitor.cs b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs index fb3d8287..8342f4fc 100644 --- a/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs +++ b/MarkMpn.Sql4Cds.Engine/Visitors/TDSEndpointCompatibilityVisitor.cs @@ -387,16 +387,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); }