Skip to content

Commit

Permalink
Merge pull request #433 from MarkMpn/executeas
Browse files Browse the repository at this point in the history
Dapper compatibility & XML improvements
  • Loading branch information
MarkMpn authored Feb 24, 2024
2 parents c6c6ae8 + 2a58c1c commit b499599
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 12 deletions.
133 changes: 128 additions & 5 deletions MarkMpn.Sql4Cds.Engine.Tests/AdoProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '<Root>
<ProductDescription ProductID=""1"" ProductName=""Road Bike"">
Expand All @@ -1161,12 +1164,12 @@ DECLARE @ProdID INT
</Root>'
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);
}
}

Expand Down Expand Up @@ -1926,5 +1929,125 @@ public void MetadataEnumConversionErrors()
}
}
}

class Account<TId>
{
public TId AccountId { get; set; }
public string Name { get; set; }
public int? Employees { get; set; }
}

class EntityReferenceTypeHandler : SqlMapper.TypeHandler<EntityReference>
{
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<EntityReference>(id => id.Id);
}

[TestMethod]
public void DapperQuerySqlEntityReference()
{
DapperQuery<SqlEntityReference>(id => id.Id);
}

[TestMethod]
public void DapperQueryGuid()
{
DapperQuery<Guid>(id => id);
}

private void DapperQuery<TId>(Func<TId,Guid> 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<Guid, Entity>
{
[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<Account<TId>>("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<SqlEntityReference>
{
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<SqlEntityReference>("SELECT @@IDENTITY");

var name = con.ExecuteScalar<string>("SELECT name FROM account WHERE accountid = @id", new { id });

Assert.AreEqual("Dapper", name);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper.StrongName">
<Version>2.1.28</Version>
</PackageReference>
<PackageReference Include="FakeXrmEasy.9">
<Version>1.58.1</Version>
</PackageReference>
Expand Down
6 changes: 3 additions & 3 deletions MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -304,7 +304,7 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
// Capture the values of output parameters
foreach (var param in Parameters.Cast<Sql4CdsParameter>().Where(p => p.Direction == ParameterDirection.Output))
param.SetOutputValue((INullable)reader.ParameterValues[param.ParameterName]);
param.SetOutputValue((INullable)reader.ParameterValues[param.FullParameterName]);
}

return reader;
Expand Down
2 changes: 2 additions & 0 deletions MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
4 changes: 2 additions & 2 deletions MarkMpn.Sql4Cds.Engine/Ado/Sql4CdsParameterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ internal Dictionary<string, DataTypeReference> GetParameterTypes()
{
return _parameters
.Cast<Sql4CdsParameter>()
.ToDictionary(param => param.ParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase);
.ToDictionary(param => param.FullParameterName, param => param.GetDataType(), StringComparer.OrdinalIgnoreCase);
}

internal Dictionary<string, object> GetParameterValues()
{
return _parameters
.Cast<Sql4CdsParameter>()
.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)
Expand Down
2 changes: 1 addition & 1 deletion MarkMpn.Sql4Cds.Engine/ExpressionFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down

0 comments on commit b499599

Please sign in to comment.