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
{
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);
}
......
......@@ -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
[Fact(Skip="Bug in Firebird; a PR to fix it has been submitted")]
public void Issue178_Firebird()
......
......@@ -10,14 +10,6 @@ sealed class DapperRowMetaObject : System.Dynamic.DynamicMetaObject
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) });
public DapperRowMetaObject(
System.Linq.Expressions.Expression expression,
System.Dynamic.BindingRestrictions restrictions
)
: base(expression, restrictions)
{
}
public DapperRowMetaObject(
System.Linq.Expressions.Expression expression,
System.Dynamic.BindingRestrictions restrictions,
......@@ -79,6 +71,12 @@ public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.Se
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()
public static void SetDefaults()
{
CommandTimeout = null;
IgnoreDuplicatedColumns = false;
}
/// <summary>
/// Specifies the default Command Timeout for all Queries
/// </summary>
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)
}
var effectiveFieldCount = Math.Min(fieldCount - startBound, length);
int firstCol = -1;
DapperTable table = null;
BitArray enabledCols = null;
return
r =>
{
if (table == null)
{
string[] names = new string[effectiveFieldCount];
for (int i = 0; i < effectiveFieldCount; i++)
string[] names;
if (Settings.IgnoreDuplicatedColumns)
{
names[i] = r.GetName(i + startBound);
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++)
{
names[i] = r.GetName(i + startBound);
}
}
table = new DapperTable(names);
}
var values = new object[effectiveFieldCount];
var values = new object[table.FieldCount];
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)
{
return null;
}
}
if (startBound == 0)
// actually load the data
int idx = firstCol;
if (returnNullIfFirstMissing) idx++; // we already read it
for (int i = idx; i < effectiveFieldCount; i++)
{
for (int i = 0; i < values.Length; i++)
if (enabledCols == null || enabledCols[i])
{
object val = r.GetValue(i);
values[i] = val is DBNull ? null : val;
}
}
else
{
var begin = returnNullIfFirstMissing ? 1 : 0;
for (var iter = begin; iter < effectiveFieldCount; ++iter)
{
object obj = r.GetValue(iter + startBound);
values[iter] = obj is DBNull ? null : obj;
object val = r.GetValue(i + startBound);
values[idx++] = val is DBNull ? null : val;
}
}
return new DapperRow(table, values);
......@@ -2445,6 +2466,22 @@ public static void SetTypeMap(Type type, ITypeMap map)
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>
/// Internal use only
/// </summary>
......@@ -2475,12 +2512,19 @@ public static void SetTypeMap(Type type, ITypeMap map)
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);
int index = startBound;
ConstructorInfo specializedConstructor = null;
#if !COREFX
......@@ -2493,12 +2537,6 @@ public static void SetTypeMap(Type type, ITypeMap map)
}
else
{
var types = new Type[length];
for (int i = startBound; i < startBound + length; i++)
{
types[i - startBound] = reader.GetFieldType(i);
}
var explicitConstr = typeMap.FindExplicitConstructor();
if (explicitConstr != null)
{
......@@ -2539,10 +2577,17 @@ public static void SetTypeMap(Type type, ITypeMap map)
}
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)
{
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");
}
......@@ -2576,17 +2621,18 @@ public static void SetTypeMap(Type type, ITypeMap map)
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]
bool first = true;
var allDone = il.DefineLabel();
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 (specializedConstructor == null)
......@@ -2595,13 +2641,13 @@ public static void SetTypeMap(Type type, ITypeMap map)
Label finishLabel = il.DefineLabel();
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.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.Dup); // stack is now [target][target][value-as-object][value-as-object]
StoreLocal(il, valueCopyLocal);
Type colType = reader.GetFieldType(index);
Type colType = col.Type;
Type memberType = item.MemberType;
if (memberType == typeof(char) || memberType == typeof(char?))
......@@ -2729,7 +2775,6 @@ public static void SetTypeMap(Type type, ITypeMap map)
il.MarkLabel(finishLabel);
}
first = false;
index += 1;
}
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