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()
parents[3].Children.Select(c => c.Id).SequenceEqual(new[] { 5 }).IsTrue();
}
private class Parent
internal class Parent
{
public int Id { get; set; }
public readonly List<Child> Children = new List<Child>();
}
private class Child
internal class Child
{
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)
&& 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
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>(
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)
{
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)
{
if (types.Length < 1)
......@@ -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)
{
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