Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Dapper/DynamicParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ public void Add(string name, object? value = null, DbType? dbType = null, Parame
};
}

private static bool IsListParameter(ParamInfo param)
{
if (param.CameFromTemplate || param.Value is null || param.Value is SqlMapper.ICustomQueryParameter) return false;
var dbType = param.DbType;
if (dbType is null)
{
#pragma warning disable 618
dbType = SqlMapper.LookupDbType(param.Value.GetType(), Clean(param.Name), true, out _);
#pragma warning restore 618
}
return dbType == EnumerableMultiParameter;
}

private static string Clean(string name)
{
if (!string.IsNullOrEmpty(name))
Expand Down Expand Up @@ -228,7 +241,9 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity)
outputCallbacks?.ForEach(generator => generator());
}

foreach (var param in parameters.Values)
// lists whose names could collide with another list's generated names must expand longest-name first
var ordered = SqlMapper.OrderForListExpansion(parameters.Values.ToList(), p => Clean(p.Name), IsListParameter);
foreach (var param in ordered)
{
if (param.CameFromTemplate) continue;

Expand Down
55 changes: 55 additions & 0 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2313,6 +2313,57 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
}
}

/// <summary>
/// List expansion rewrites the command text one list parameter at a time, replacing <c>@name</c> with
/// <c>(@name1,@name2,...)</c>. If list <c>values1</c> is expanded before list <c>values11</c>, the generated
/// <c>@values11</c> is indistinguishable from the original <c>@values11</c> placeholder, so the later
/// expansion rewrites the wrong token. Token matching cannot tell them apart; only expansion order can.
/// A generated name is always longer than its list name and extends it with digits, so expanding longer
/// names first can never produce a token that a later (shorter) list name matches. Lists are only
/// reordered (among themselves, leaving other parameters in place) when such a collision is possible.
/// </summary>
internal static IList<T> OrderForListExpansion<T>(IList<T> items, Func<T, string> getName, Func<T, bool> isList)
{
List<int>? slots = null;
for (int i = 0; i < items.Count; i++)
{
if (isList(items[i])) (slots ??= new List<int>()).Add(i);
}
if (slots is null || slots.Count < 2) return items;

bool collides = false;
for (int a = 0; a < slots.Count && !collides; a++)
{
var shorter = getName(items[slots[a]]);
for (int b = 0; b < slots.Count; b++)
{
var longer = getName(items[slots[b]]);
if (longer.Length > shorter.Length
&& longer.StartsWith(shorter, StringComparison.OrdinalIgnoreCase)
&& AllDigits(longer, shorter.Length))
{
collides = true;
break;
}
}
}
if (!collides) return items;

var sorted = slots.Select(i => items[i]).OrderByDescending(x => getName(x).Length).ToList();
var result = new List<T>(items);
for (int i = 0; i < slots.Count; i++) result[slots[i]] = sorted[i];
return result;

static bool AllDigits(string s, int from)
{
for (int i = from; i < s.Length; i++)
{
if (s[i] < '0' || s[i] > '9') return false;
}
return true;
}
}

private static bool TryStringSplit(ref IEnumerable? list, int splitAt, string namePrefix, IDbCommand command, bool byPosition)
{
if (list is null || splitAt < 0) return false;
Expand Down Expand Up @@ -2668,6 +2719,10 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
props = FilterParameters(props, identity.Sql);
}

#pragma warning disable 618
props = OrderForListExpansion<PropertyInfo>(props.ToList(), p => p.Name, p => LookupDbType(p.PropertyType, p.Name, true, out _) == DynamicParameters.EnumerableMultiParameter);
#pragma warning restore 618

var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt;
foreach (var prop in props)
{
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<PackageVersion Include="Norm.net" Version="5.4.0" />
<PackageVersion Include="Npgsql" Version="10.0.2" />
<PackageVersion Include="PetaPoco" Version="5.1.306" />
<PackageVersion Include="pengdows.crud.fakeDb" Version="2.0.5" />
<PackageVersion Include="RepoDb.SqlServer" Version="1.13.1" />
<PackageVersion Include="ServiceStack.OrmLite.SqlServer" Version="10.0.6" />
<PackageVersion Include="Snowflake.Data" Version="5.5.0" />
Expand Down
5 changes: 3 additions & 2 deletions tests/Dapper.Tests/Dapper.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
<PackageReference Include="log4net" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net481'">
<PackageReference Include="Npgsql" />
<ItemGroup Condition="'$(TargetFramework)' != 'net481'">
<PackageReference Include="pengdows.crud.fakeDb" />
<PackageReference Include="Npgsql" />
<PackageReference Include="Snowflake.Data" />
</ItemGroup>

Expand Down
78 changes: 78 additions & 0 deletions tests/Dapper.Tests/FakeDbTests.Issue1040.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#if !NET481
using System.Collections.Generic;
using pengdows.crud.fakeDb;
using Xunit;

namespace Dapper.Tests
{
public sealed class FakeDbIssue1040Tests
{
private static string Run(string sql, object args)
{
using var conn = new fakeDbConnection(new FakeDataStore());
conn.EnqueueNonQueryResult(1);
conn.Open();
conn.Execute(sql, args);
return Assert.Single(conn.ExecutedNonQueryTexts);
}

[Fact]
public void ListExpansionDoesNotCollideWithLongerListName()
{
var text = Run(
"select 1 where Value in @values1 or Value in @values11",
new { values1 = new[] { 1, 2 }, values11 = new[] { 3, 4 } });
Assert.Equal("select 1 where Value in (@values11,@values12) or Value in (@values111,@values112)", text);
}

[Fact]
public void ListExpansionIsIndependentOfDeclarationOrder()
{
var text = Run(
"select 1 where Value in @values11 or Value in @values1",
new { values11 = new[] { 3, 4 }, values1 = new[] { 1, 2 } });
Assert.Equal("select 1 where Value in (@values111,@values112) or Value in (@values11,@values12)", text);
}

[Fact]
public void ThreeOverlappingListNamesExpandIndependently()
{
var text = Run(
"select 1 where a in @v or b in @v1 or c in @v11",
new { v = new[] { 1, 2 }, v1 = new[] { 3, 4 }, v11 = new[] { 5, 6 } });
Assert.Equal("select 1 where a in (@v1,@v2) or b in (@v11,@v12) or c in (@v111,@v112)", text);
}

[Fact]
public void DynamicParametersListExpansionDoesNotCollide()
{
var args = new DynamicParameters();
args.Add("values1", new[] { 1, 2 });
args.Add("values11", new[] { 3, 4 });
var text = Run("select 1 where Value in @values1 or Value in @values11", args);
Assert.Equal("select 1 where Value in (@values11,@values12) or Value in (@values111,@values112)", text);
}

[Fact]
public void DictionaryListExpansionDoesNotCollide()
{
var args = new Dictionary<string, object>
{
["values1"] = new[] { 1, 2 },
["values11"] = new[] { 3, 4 },
};
var text = Run("select 1 where Value in @values1 or Value in @values11", args);
Assert.Equal("select 1 where Value in (@values11,@values12) or Value in (@values111,@values112)", text);
}

[Fact]
public void UnrelatedListNamesKeepOrderAndExpandNormally()
{
var text = Run(
"select 1 where a in @ids or b in @codes",
new { ids = new[] { 1, 2 }, codes = new[] { 3 } });
Assert.Equal("select 1 where a in (@ids1,@ids2) or b in (@codes1)", text);
}
}
}
#endif