Commit 2df6d074 authored by Marc Gravell's avatar Marc Gravell

Experimental multi-map code for tuples (not optimized)

parent 841b03f2
...@@ -29,13 +29,13 @@ public void ParentChildIdentityAssociations() ...@@ -29,13 +29,13 @@ public void ParentChildIdentityAssociations()
parents[3].Children.Select(c => c.Id).SequenceEqual(new[] { 5 }).IsTrue(); parents[3].Children.Select(c => c.Id).SequenceEqual(new[] { 5 }).IsTrue();
} }
private class Parent internal class Parent
{ {
public int Id { get; set; } public int Id { get; set; }
public readonly List<Child> Children = new List<Child>(); public readonly List<Child> Children = new List<Child>();
} }
private class Child internal class Child
{ {
public int Id { get; set; } public int Id { get; set; }
} }
......
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Child = Dapper.Tests.MultiMapTests.Child;
using Parent = Dapper.Tests.MultiMapTests.Parent;
namespace Dapper.Tests
{
public class MultiMapTupleTests : TestBase
{
// note: implementation is not optimized yet - just basic reflection, no IL crazy
// intent: to explore the API, allowing horizontally partitioned data to be fetched
// more convenienty by expressing them as tuples; this is similar to the (hard to use)
// pre-existing multi-generic Query<...> API that does the same, i.e.
// similar to connection.Query<Parent,Child,Parent>(...) - which folks find hard to grok
// here are 3 possible ideas for expressing that
[Fact]
public void GetRawTuples_Manual() // here we use an explicit map function that returns the entire row, and
// in doing so provides the contextual name metadata
{
var tuples = connection.Query(
@"select 1 as [Id], 1 as [Id] union all select 1,2 union all select 2,3 union all select 1,4 union all select 3,5",
((Parent parent, Child child)row) => row
).AsList();
tuples.Count.IsEqualTo(5);
string.Join(",", tuples.Select(x => $"({x.parent.Id},{x.child.Id})")).IsEqualTo(
"(1,1),(1,2),(2,3),(1,4),(3,5)");
}
[Fact]
public void GetRawTuples_Passthru() // here we use a declarative map function, so all we provide is the T to Map<T>
{
var tuples = connection.Query(
@"select 1 as [Id], 1 as [Id] union all select 1,2 union all select 2,3 union all select 1,4 union all select 3,5",
SqlMapper.Map<(Parent parent, Child child)>()
).AsList();
tuples.Count.IsEqualTo(5);
string.Join(",", tuples.Select(x => $"({x.parent.Id},{x.child.Id})")).IsEqualTo(
"(1,1),(1,2),(2,3),(1,4),(3,5)");
}
[Fact]
public void GetRawTuples_Split() // here we provide the tuple metadata via the T in QuerySplit<T> - note the name
// is different to avoid ambiguity with the primary Query<T> which does something else
{
var tuples = connection.QuerySplit<(Parent parent, Child child)>(
@"select 1 as [Id], 1 as [Id] union all select 1,2 union all select 2,3 union all select 1,4 union all select 3,5"
).AsList();
tuples.Count.IsEqualTo(5);
string.Join(",", tuples.Select(x => $"({x.parent.Id},{x.child.Id})")).IsEqualTo(
"(1,1),(1,2),(2,3),(1,4),(3,5)");
}
// these are more complex examples that make use of a non-trivial mapping function to play with the horizontal partitions *before*
// yielding them - for example, to take parent/child data and stitch it together such that the children are attached to the parents
// compare and contrast: MultiMapTests.ParentChildIdentityAssociations
[Fact]
public void ParentChildIdentityAssociations()
{
var lookup = new Dictionary<int, Parent>();
var parents = connection.Query(@"select 1 as [Id], 1 as [Id] union all select 1,2 union all select 2,3 union all select 1,4 union all select 3,5",
((Parent parent, Child child) row) =>
{
if (!lookup.TryGetValue(row.parent.Id, out Parent found))
{
lookup.Add(row.parent.Id, found = row.parent);
}
found.Children.Add(row.child);
return found;
}).Distinct().ToDictionary(p => p.Id);
parents.Count.IsEqualTo(3);
parents[1].Children.Select(c => c.Id).SequenceEqual(new[] { 1, 2, 4 }).IsTrue();
parents[2].Children.Select(c => c.Id).SequenceEqual(new[] { 3 }).IsTrue();
parents[3].Children.Select(c => c.Id).SequenceEqual(new[] { 5 }).IsTrue();
}
// compare and contrast: MultiMapTests.TestMultiMap
[Fact]
public void TestMultiMap()
{
const string createSql = @"
create table #Users (Id int, Name varchar(20))
create table #Posts (Id int, OwnerId int, Content varchar(20))
insert #Users values(99, 'Sam')
insert #Users values(2, 'I am')
insert #Posts values(1, 99, 'Sams Post1')
insert #Posts values(2, 99, 'Sams Post2')
insert #Posts values(3, null, 'no ones post')
";
connection.Execute(createSql);
try
{
const string sql =
@"select * from #Posts p
left join #Users u on u.Id = p.OwnerId
Order by p.Id";
var data = connection.Query(sql, ((Post post, User user) row) => { row.post.Owner = row.user; return row.post; }).ToList();
var p = data.First();
p.Content.IsEqualTo("Sams Post1");
p.Id.IsEqualTo(1);
p.Owner.Name.IsEqualTo("Sam");
p.Owner.Id.IsEqualTo(99);
data[2].Owner.IsNull();
}
finally
{
connection.Execute("drop table #Users drop table #Posts");
}
}
}
}
...@@ -125,5 +125,10 @@ public bool Equals(Identity other) ...@@ -125,5 +125,10 @@ public bool Equals(Identity other)
&& parametersType == other.parametersType; && parametersType == other.parametersType;
} }
} }
/// <summary>
/// Exposes a pass-thru identity map (useful for using with multi-map and tuples)
/// </summary>
public static Func<T, T> Map<T>() where T : struct => x => x;
} }
} }
...@@ -1271,6 +1271,23 @@ public static IEnumerable<TReturn> Query<TReturn>(this IDbConnection cnn, string ...@@ -1271,6 +1271,23 @@ public static IEnumerable<TReturn> Query<TReturn>(this IDbConnection cnn, string
return buffered ? results.ToList() : results; return buffered ? results.ToList() : results;
} }
/// <summary>
/// Perform a multi mapping query with arbitrary input parameters expressed as tuples
/// </summary>
public static IEnumerable<TReturn> Query<TTuple, TReturn>(this IDbConnection cnn, string sql, Func<TTuple, TReturn> map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
where TTuple : struct
{
var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None);
var results = MultiMapImpl<TTuple, TReturn>(cnn, command, map, splitOn, null, null, true);
return buffered ? results.ToList() : results;
}
/// <summary>
/// Perform a multi mapping query with arbitrary input parameters expressed as tuples
/// </summary>
public static IEnumerable<TTuple> QuerySplit<TTuple>(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
where TTuple : struct
=> Query<TTuple, TTuple>(cnn, sql, t => t, param, transaction, buffered, splitOn, commandTimeout, commandType);
static IEnumerable<TReturn> MultiMap<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>( static IEnumerable<TReturn> MultiMap<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(
this IDbConnection cnn, string sql, Delegate map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) this IDbConnection cnn, string sql, Delegate map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType)
{ {
...@@ -1343,6 +1360,103 @@ private static CommandBehavior GetBehavior(bool close, CommandBehavior @default) ...@@ -1343,6 +1360,103 @@ private static CommandBehavior GetBehavior(bool close, CommandBehavior @default)
{ {
return (close ? (@default | CommandBehavior.CloseConnection) : @default) & Settings.AllowedCommandBehaviors; return (close ? (@default | CommandBehavior.CloseConnection) : @default) & Settings.AllowedCommandBehaviors;
} }
static class TupleCache<T>
{
private static readonly Type[] _types;
private static readonly ConstructorInfo _ctor;
public static Type[] GetTypes()
=> _types ?? throw new InvalidOperationException($"Type {typeof(T)} is not a tuple");
public static ConstructorInfo Constructor
=> _ctor ?? throw new InvalidOperationException($"Type {typeof(T)} is not a tuple");
static TupleCache()
{
var type = typeof(T);
if(IsValueTuple(type))
{
foreach(var ctor in type.GetConstructors())
{
var parameters = ctor.GetParameters();
if(parameters.Length != 0)
{
Type[] types = new Type[parameters.Length];
for (int i = 0; i < types.Length; i++)
types[i] = parameters[i].ParameterType;
_types = types;
_ctor = ctor;
break;
}
}
}
}
}
static IEnumerable<TReturn> MultiMapImpl<TTuple, TReturn>(this IDbConnection cnn, CommandDefinition command, Func<TTuple, TReturn> map, string splitOn, IDataReader reader, Identity identity, bool finalize)
where TTuple : struct
{
Type[] types = TupleCache<TTuple>.GetTypes();
if (types.Length < 1)
{
throw new ArgumentException("you must provide at least one type to deserialize");
}
object param = command.Parameters;
identity = identity ?? new Identity(command.CommandText, command.CommandType, cnn, types[0], param?.GetType(), types);
CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache);
IDbCommand ownedCommand = null;
IDataReader ownedReader = null;
bool wasClosed = cnn != null && cnn.State == ConnectionState.Closed;
try
{
if (reader == null)
{
ownedCommand = command.SetupCommand(cnn, cinfo.ParamReader);
if (wasClosed) cnn.Open();
ownedReader = ExecuteReaderWithFlagsFallback(ownedCommand, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult);
reader = ownedReader;
}
DeserializerState deserializer;
Func<IDataReader, object>[] otherDeserializers;
int hash = GetColumnHash(reader);
if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash)
{
var deserializers = GenerateDeserializers(types, splitOn, reader);
deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]);
otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray();
SetQueryCache(identity, cinfo);
}
Func<IDataReader, TReturn> mapIt = GenerateMapper<TTuple, TReturn>(deserializer.Func, otherDeserializers, TupleCache<TTuple>.Constructor, map);
if (mapIt != null)
{
while (reader.Read())
{
yield return mapIt(reader);
}
if (finalize)
{
while (reader.NextResult()) { }
command.OnCompleted();
}
}
}
finally
{
try
{
ownedReader?.Dispose();
}
finally
{
ownedCommand?.Dispose();
if (wasClosed) cnn.Close();
}
}
}
static IEnumerable<TReturn> MultiMapImpl<TReturn>(this IDbConnection cnn, CommandDefinition command, Type[] types, Func<object[], TReturn> map, string splitOn, IDataReader reader, Identity identity, bool finalize) static IEnumerable<TReturn> MultiMapImpl<TReturn>(this IDbConnection cnn, CommandDefinition command, Type[] types, Func<object[], TReturn> map, string splitOn, IDataReader reader, Identity identity, bool finalize)
{ {
if (types.Length < 1) if (types.Length < 1)
...@@ -1407,7 +1521,20 @@ static IEnumerable<TReturn> MultiMapImpl<TReturn>(this IDbConnection cnn, Comman ...@@ -1407,7 +1521,20 @@ static IEnumerable<TReturn> MultiMapImpl<TReturn>(this IDbConnection cnn, Comman
} }
} }
} }
private static Func<IDataReader, TReturn> GenerateMapper<TTuple, TReturn>(Func<IDataReader, object> deserializer, Func<IDataReader, object>[] otherDeserializers, ConstructorInfo ctor, Func<TTuple, TReturn> map)
where TTuple : struct
{
return r =>
{
// unoptimized, obvs!
object[] args = new object[1 + otherDeserializers.Length];
args[0] = deserializer(r);
for (int i = 1; i < args.Length; i++)
args[i] = otherDeserializers[i - 1](r);
var tuple = (TTuple)ctor.Invoke(args);
return map(tuple);
};
}
private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Func<IDataReader, object> deserializer, Func<IDataReader, object>[] otherDeserializers, object map) private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Func<IDataReader, object> deserializer, Func<IDataReader, object>[] otherDeserializers, object map)
{ {
switch (otherDeserializers.Length) switch (otherDeserializers.Length)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment