Unverified Commit 2c81a408 authored by JulianRooze's avatar JulianRooze Committed by GitHub

Fix handling of selecting nullable value tuples (#1400)

`SingleOrDefault<(int, int)?>(query)` always returned null because the IsValueTuple did not handle nullable types and thus the `GenerateValueTupleDeserializer` method was never called.

I've added test for the case when the query does not return something (the tuple should be null) and when it does return something. Additionally added tests for the edge-case where you select 8 or 15 element tuples to make sure nothing broke there. 
parent 0e04fe1d
...@@ -40,6 +40,22 @@ public void TupleReturnValue_TooManyColumns_Ignored() ...@@ -40,6 +40,22 @@ public void TupleReturnValue_TooManyColumns_Ignored()
Assert.Equal("Fred", val.name); Assert.Equal("Fred", val.name);
} }
[Fact]
public void TupleReturnValue_NullableTuple_Works()
{
var val = connection.QuerySingleOrDefault<(int id, string name)?>("select 42, 'Fred', 123");
Assert.NotNull(val);
Assert.Equal(42, val.Value.id);
Assert.Equal("Fred", val.Value.name);
}
[Fact]
public void TupleReturnValue_NullableTuple_Works_When_Null()
{
var val = connection.QuerySingleOrDefault<(int id, string name)?>("select 42, 'Fred', 123 where 1 = 2");
Assert.Null(val);
}
[Fact] [Fact]
public void TupleReturnValue_TooFewColumns_Unmapped() public void TupleReturnValue_TooFewColumns_Unmapped()
{ {
...@@ -75,7 +91,37 @@ public void TupleReturnValue_Works_With8Elements() ...@@ -75,7 +91,37 @@ public void TupleReturnValue_Works_With8Elements()
Assert.Equal(7, val.e7); Assert.Equal(7, val.e7);
Assert.Equal(8, val.e8); Assert.Equal(8, val.e8);
} }
[Fact]
public void Nullable_TupleReturnValue_Works_With8Elements()
{
// C# encodes an 8-tuple as ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8>>
var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8)?>(
"select 1, 2, 3, 4, 5, 6, 7, 8");
Assert.NotNull(val);
Assert.Equal(1, val.Value.e1);
Assert.Equal(2, val.Value.e2);
Assert.Equal(3, val.Value.e3);
Assert.Equal(4, val.Value.e4);
Assert.Equal(5, val.Value.e5);
Assert.Equal(6, val.Value.e6);
Assert.Equal(7, val.Value.e7);
Assert.Equal(8, val.Value.e8);
}
[Fact]
public void Nullable_TupleReturnValue_Works_With8Elements_When_Null()
{
// C# encodes an 8-tuple as ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8>>
var val = connection.QuerySingleOrDefault<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8)?>(
"select 1, 2, 3, 4, 5, 6, 7, 8 where 1 = 2");
Assert.Null(val);
}
[Fact] [Fact]
public void TupleReturnValue_Works_With15Elements() public void TupleReturnValue_Works_With15Elements()
{ {
...@@ -101,6 +147,44 @@ public void TupleReturnValue_Works_With15Elements() ...@@ -101,6 +147,44 @@ public void TupleReturnValue_Works_With15Elements()
Assert.Equal(15, val.e15); Assert.Equal(15, val.e15);
} }
[Fact]
public void Nullable_TupleReturnValue_Works_With15Elements()
{
// C# encodes a 15-tuple as ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8, T9, T10, T11, T12, T13, T14, ValueTuple<T15>>>
var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8, int e9, int e10, int e11, int e12, int e13, int e14, int e15)?>(
"select 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15");
Assert.NotNull(val);
Assert.Equal(1, val.Value.e1);
Assert.Equal(2, val.Value.e2);
Assert.Equal(3, val.Value.e3);
Assert.Equal(4, val.Value.e4);
Assert.Equal(5, val.Value.e5);
Assert.Equal(6, val.Value.e6);
Assert.Equal(7, val.Value.e7);
Assert.Equal(8, val.Value.e8);
Assert.Equal(9, val.Value.e9);
Assert.Equal(10, val.Value.e10);
Assert.Equal(11, val.Value.e11);
Assert.Equal(12, val.Value.e12);
Assert.Equal(13, val.Value.e13);
Assert.Equal(14, val.Value.e14);
Assert.Equal(15, val.Value.e15);
}
[Fact]
public void Nullable_TupleReturnValue_Works_With15Elements_When_Null()
{
// C# encodes a 15-tuple as ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8, T9, T10, T11, T12, T13, T14, ValueTuple<T15>>>
var val = connection.QuerySingleOrDefault<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8, int e9, int e10, int e11, int e12, int e13, int e14, int e15)?>(
"select 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 where 1 = 2");
Assert.Null(val);
}
[Fact] [Fact]
public void TupleReturnValue_Works_WithStringField() public void TupleReturnValue_Works_WithStringField()
{ {
......
...@@ -2362,7 +2362,9 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql) ...@@ -2362,7 +2362,9 @@ internal static IList<LiteralToken> GetLiteralTokens(string sql)
public static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) => public static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) =>
CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql)); CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql));
private static bool IsValueTuple(Type type) => type?.IsValueType == true && type.FullName.StartsWith("System.ValueTuple`", StringComparison.Ordinal); private static bool IsValueTuple(Type type) => (type?.IsValueType == true
&& type.FullName.StartsWith("System.ValueTuple`", StringComparison.Ordinal))
|| (type != null && IsValueTuple(Nullable.GetUnderlyingType(type)));
internal static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals) internal static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals)
{ {
...@@ -3078,7 +3080,8 @@ private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary<Type, Lo ...@@ -3078,7 +3080,8 @@ private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary<Type, Lo
private static void GenerateValueTupleDeserializer(Type valueTupleType, IDataReader reader, int startBound, int length, ILGenerator il) private static void GenerateValueTupleDeserializer(Type valueTupleType, IDataReader reader, int startBound, int length, ILGenerator il)
{ {
var currentValueTupleType = valueTupleType; var nullableUnderlyingType = Nullable.GetUnderlyingType(valueTupleType);
var currentValueTupleType = nullableUnderlyingType ?? valueTupleType;
var constructors = new List<ConstructorInfo>(); var constructors = new List<ConstructorInfo>();
var languageTupleElementTypes = new List<Type>(); var languageTupleElementTypes = new List<Type>();
...@@ -3163,6 +3166,13 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, IDataRea ...@@ -3163,6 +3166,13 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, IDataRea
il.Emit(OpCodes.Newobj, constructors[i]); il.Emit(OpCodes.Newobj, constructors[i]);
} }
if (nullableUnderlyingType != null)
{
var nullableTupleConstructor = valueTupleType.GetConstructor(new[] { nullableUnderlyingType });
il.Emit(OpCodes.Newobj, nullableTupleConstructor);
}
il.Emit(OpCodes.Box, valueTupleType); il.Emit(OpCodes.Box, valueTupleType);
il.Emit(OpCodes.Ret); il.Emit(OpCodes.Ret);
} }
......
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