Commit 9aadf7f6 authored by Marc Gravell's avatar Marc Gravell

Implement opt-in IgnoreDuplicatedColumns feature; if grid returns {X,Y,X},...

Implement opt-in IgnoreDuplicatedColumns feature; if grid returns {X,Y,X}, only the first X is included
parent e24c472f
...@@ -4,7 +4,7 @@ namespace Dapper.Tests ...@@ -4,7 +4,7 @@ namespace Dapper.Tests
{ {
public static class Assert public static class Assert
{ {
public static void IsEqualTo<T>(this T expected, T actual) public static void IsEqualTo<T>(this T actual, T expected)
{ {
Xunit.Assert.Equal(expected, actual); Xunit.Assert.Equal(expected, actual);
} }
......
...@@ -2845,6 +2845,44 @@ public void SO30435185_InvalidTypeOwner() ...@@ -2845,6 +2845,44 @@ public void SO30435185_InvalidTypeOwner()
} }
} }
[Fact]
public void Issue326_RepeatedColumnWinners_Original()
{
RunRepeatedColumnWinners(false);
}
[Fact]
public void Issue326_RepeatedColumnWinners_FirstColumnWins()
{
RunRepeatedColumnWinners(true);
}
class Issue326
{
public int Id { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }
}
private void RunRepeatedColumnWinners(bool newBehavior)
{
var oldValue = Dapper.SqlMapper.Settings.IgnoreDuplicatedColumns;
Dapper.SqlMapper.Settings.IgnoreDuplicatedColumns = newBehavior;
try
{
var row = connection.QueryFirstOrDefault<Issue326>("select 1 as Id, 'abc' as Name, 2 as Id, 'def' as Description");
row.Id.IsEqualTo(newBehavior ? 1 : 2);
row.Name.IsEqualTo("abc");
row.Description.IsEqualTo("def");
var dyn = connection.QueryFirstOrDefault("select 3 as Id, 'ghi' as Name, 4 as Id, 'jkl' as Description");
((int)dyn.Id).IsEqualTo(3); // note: dynamic, old behavior was to pick the first, since that won the IndexOf race
((string)dyn.Name).IsEqualTo("ghi");
((string)dyn.Description).IsEqualTo("jkl");
}
finally
{
Dapper.SqlMapper.Settings.IgnoreDuplicatedColumns = oldValue;
}
}
#if EXTERNALS #if EXTERNALS
[Fact(Skip="Bug in Firebird; a PR to fix it has been submitted")] [Fact(Skip="Bug in Firebird; a PR to fix it has been submitted")]
public void Issue178_Firebird() public void Issue178_Firebird()
......
...@@ -10,14 +10,6 @@ sealed class DapperRowMetaObject : System.Dynamic.DynamicMetaObject ...@@ -10,14 +10,6 @@ sealed class DapperRowMetaObject : System.Dynamic.DynamicMetaObject
static readonly MethodInfo getValueMethod = typeof(IDictionary<string, object>).GetProperty("Item").GetGetMethod(); static readonly MethodInfo getValueMethod = typeof(IDictionary<string, object>).GetProperty("Item").GetGetMethod();
static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) }); static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) });
public DapperRowMetaObject(
System.Linq.Expressions.Expression expression,
System.Dynamic.BindingRestrictions restrictions
)
: base(expression, restrictions)
{
}
public DapperRowMetaObject( public DapperRowMetaObject(
System.Linq.Expressions.Expression expression, System.Linq.Expressions.Expression expression,
System.Dynamic.BindingRestrictions restrictions, System.Dynamic.BindingRestrictions restrictions,
...@@ -79,6 +71,12 @@ public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.Se ...@@ -79,6 +71,12 @@ public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.Se
return callMethod; return callMethod;
} }
public override IEnumerable<string> GetDynamicMemberNames()
{
DapperTable table = base.Value as DapperTable;
return table == null ? NoNames : table.FieldNames;
}
private static readonly string[] NoNames = new string[0];
} }
} }
} }
...@@ -18,12 +18,30 @@ static Settings() ...@@ -18,12 +18,30 @@ static Settings()
public static void SetDefaults() public static void SetDefaults()
{ {
CommandTimeout = null; CommandTimeout = null;
IgnoreDuplicatedColumns = false;
} }
/// <summary> /// <summary>
/// Specifies the default Command Timeout for all Queries /// Specifies the default Command Timeout for all Queries
/// </summary> /// </summary>
public static int? CommandTimeout { get; set; } public static int? CommandTimeout { get; set; }
private static bool ignoreDuplicatedColumns;
/// <summary>
/// If a column name is duplicated, the duplicates are skipped (by default, all are processed, so the last column wins)
/// </summary>
/// <remarks>This setting should be set once at the start of the application; it is not intended to be toggled per-query</remarks>
public static bool IgnoreDuplicatedColumns {
get { return ignoreDuplicatedColumns; }
set
{
if (value != ignoreDuplicatedColumns)
{
ignoreDuplicatedColumns = value;
PurgeQueryCache();
}
}
}
} }
} }
} }
...@@ -1464,48 +1464,69 @@ private static Exception MultiMapException(IDataRecord reader) ...@@ -1464,48 +1464,69 @@ private static Exception MultiMapException(IDataRecord reader)
} }
var effectiveFieldCount = Math.Min(fieldCount - startBound, length); var effectiveFieldCount = Math.Min(fieldCount - startBound, length);
int firstCol = -1;
DapperTable table = null; DapperTable table = null;
BitArray enabledCols = null;
return return
r => r =>
{ {
if (table == null) if (table == null)
{ {
string[] names = new string[effectiveFieldCount]; string[] names;
if (Settings.IgnoreDuplicatedColumns)
{
enabledCols = new BitArray(effectiveFieldCount);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
List<string> namesList = new List<string>(effectiveFieldCount);
for (int i = 0; i < effectiveFieldCount; i++)
{
var name = r.GetName(i + startBound);
if (seen.Add(name))
{
namesList.Add(name);
enabledCols[i] = true;
if (firstCol == -1) firstCol = i;
}
}
names = namesList.ToArray();
}
else
{
firstCol = 0;
names = new string[effectiveFieldCount];
for (int i = 0; i < effectiveFieldCount; i++) for (int i = 0; i < effectiveFieldCount; i++)
{ {
names[i] = r.GetName(i + startBound); names[i] = r.GetName(i + startBound);
} }
}
table = new DapperTable(names); table = new DapperTable(names);
} }
var values = new object[effectiveFieldCount]; var values = new object[table.FieldCount];
if (returnNullIfFirstMissing) if (returnNullIfFirstMissing)
{ {
values[0] = r.GetValue(startBound); if (Settings.IgnoreDuplicatedColumns)
System.Diagnostics.Debugger.Break();
if (firstCol == -1) return null;
values[0] = r.GetValue(startBound + firstCol);
if (values[0] is DBNull) if (values[0] is DBNull)
{ {
return null; return null;
} }
} }
if (startBound == 0) // actually load the data
{ int idx = firstCol;
for (int i = 0; i < values.Length; i++) if (returnNullIfFirstMissing) idx++; // we already read it
{ for (int i = idx; i < effectiveFieldCount; i++)
object val = r.GetValue(i);
values[i] = val is DBNull ? null : val;
}
}
else
{ {
var begin = returnNullIfFirstMissing ? 1 : 0; if (enabledCols == null || enabledCols[i])
for (var iter = begin; iter < effectiveFieldCount; ++iter)
{ {
object obj = r.GetValue(iter + startBound); object val = r.GetValue(i + startBound);
values[iter] = obj is DBNull ? null : obj; values[idx++] = val is DBNull ? null : val;
} }
} }
return new DapperRow(table, values); return new DapperRow(table, values);
...@@ -2445,6 +2466,22 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2445,6 +2466,22 @@ public static void SetTypeMap(Type type, ITypeMap map)
PurgeQueryCacheByType(type); PurgeQueryCacheByType(type);
} }
struct ColumnInfo
{
public ColumnInfo(string name, Type type, int ordinal)
{
this.name = name;
this.type = type;
this.ordinal = ordinal;
}
private string name;
private Type type;
private int ordinal;
public string Name { get { return name; } }
public Type Type { get { return type; } }
public int Ordinal { get { return ordinal; } }
}
/// <summary> /// <summary>
/// Internal use only /// Internal use only
/// </summary> /// </summary>
...@@ -2475,12 +2512,19 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2475,12 +2512,19 @@ public static void SetTypeMap(Type type, ITypeMap map)
throw MultiMapException(reader); throw MultiMapException(reader);
} }
var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); List<ColumnInfo> cols = new List<ColumnInfo>(length);
HashSet<string> uniqueNames = Settings.IgnoreDuplicatedColumns ? new HashSet<string>(StringComparer.OrdinalIgnoreCase) : null;
{
int ordinal = startBound - 1;
for (int i = 0; i < length; i++)
{
var name = reader.GetName(++ordinal);
if (uniqueNames != null && !uniqueNames.Add(name)) continue; // seen it before
cols.Add(new ColumnInfo(name, reader.GetFieldType(ordinal), ordinal));
}
}
ITypeMap typeMap = GetTypeMap(type); ITypeMap typeMap = GetTypeMap(type);
int index = startBound;
ConstructorInfo specializedConstructor = null; ConstructorInfo specializedConstructor = null;
#if !COREFX #if !COREFX
...@@ -2493,12 +2537,6 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2493,12 +2537,6 @@ public static void SetTypeMap(Type type, ITypeMap map)
} }
else else
{ {
var types = new Type[length];
for (int i = startBound; i < startBound + length; i++)
{
types[i - startBound] = reader.GetFieldType(i);
}
var explicitConstr = typeMap.FindExplicitConstructor(); var explicitConstr = typeMap.FindExplicitConstructor();
if (explicitConstr != null) if (explicitConstr != null)
{ {
...@@ -2539,10 +2577,17 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2539,10 +2577,17 @@ public static void SetTypeMap(Type type, ITypeMap map)
} }
else else
{ {
var ctor = typeMap.FindConstructor(names, types); Type[] types = new Type[cols.Count];
string[] argNames = new string[cols.Count];
for (int i = 0; i < types.Length; i++)
{
types[i] = cols[i].Type;
argNames[i] = cols[i].Name;
}
var ctor = typeMap.FindConstructor(argNames, types);
if (ctor == null) if (ctor == null)
{ {
string proposedTypes = $"({string.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray())})"; string proposedTypes = $"({string.Join(", ", types.Select((t, i) => t.FullName + " " + argNames[i]).ToArray())})";
throw new InvalidOperationException($"A parameterless default constructor or one matching signature {proposedTypes} is required for {type.FullName} materialization"); throw new InvalidOperationException($"A parameterless default constructor or one matching signature {proposedTypes} is required for {type.FullName} materialization");
} }
...@@ -2576,17 +2621,18 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2576,17 +2621,18 @@ public static void SetTypeMap(Type type, ITypeMap map)
il.Emit(OpCodes.Ldloc_1);// [target] il.Emit(OpCodes.Ldloc_1);// [target]
} }
var members = (specializedConstructor != null
? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n))
: names.Select(n => typeMap.GetMember(n))).ToList();
// stack is now [target] // stack is now [target]
bool first = true; bool first = true;
var allDone = il.DefineLabel(); var allDone = il.DefineLabel();
int enumDeclareLocal = -1, valueCopyLocal = il.DeclareLocal(typeof(object)).LocalIndex; int enumDeclareLocal = -1, valueCopyLocal = il.DeclareLocal(typeof(object)).LocalIndex;
foreach (var item in members)
foreach (var col in cols)
{ {
IMemberMap item = specializedConstructor == null
? typeMap.GetMember(col.Name)
: typeMap.GetConstructorParameter(specializedConstructor, col.Name);
if (item != null) if (item != null)
{ {
if (specializedConstructor == null) if (specializedConstructor == null)
...@@ -2595,13 +2641,13 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2595,13 +2641,13 @@ public static void SetTypeMap(Type type, ITypeMap map)
Label finishLabel = il.DefineLabel(); Label finishLabel = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader] il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader]
EmitInt32(il, index); // stack is now [target][target][reader][index] EmitInt32(il, col.Ordinal); // stack is now [target][target][reader][index]
il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index] il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index]
il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index] il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index]
il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object] il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object]
il.Emit(OpCodes.Dup); // stack is now [target][target][value-as-object][value-as-object] il.Emit(OpCodes.Dup); // stack is now [target][target][value-as-object][value-as-object]
StoreLocal(il, valueCopyLocal); StoreLocal(il, valueCopyLocal);
Type colType = reader.GetFieldType(index); Type colType = col.Type;
Type memberType = item.MemberType; Type memberType = item.MemberType;
if (memberType == typeof(char) || memberType == typeof(char?)) if (memberType == typeof(char) || memberType == typeof(char?))
...@@ -2729,7 +2775,6 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2729,7 +2775,6 @@ public static void SetTypeMap(Type type, ITypeMap map)
il.MarkLabel(finishLabel); il.MarkLabel(finishLabel);
} }
first = false; first = false;
index += 1;
} }
if (type.IsValueType()) if (type.IsValueType())
{ {
......
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