diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index 98d8f11eb..af5febfb8 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -5,7 +5,7 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net8.0;net10.0 + net461;netstandard2.0;net8.0 enable true diff --git a/Directory.Packages.props b/Directory.Packages.props index 8787a7767..edda4502c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -46,6 +50,7 @@ + diff --git a/global.json b/global.json index f7b5e40c2..7da276347 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.102", + "version": "8.0.100", "rollForward": "latestMajor" } } \ No newline at end of file diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index e02bb4ba3..a7464d32e 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -2,7 +2,7 @@ Dapper.Tests Dapper Core Test Suite - net481;net8.0;net10.0 + net481;net8.0 $(DefineConstants);MSSQLCLIENT $(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208;CA1861 enable @@ -14,6 +14,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -27,12 +31,13 @@ + - + diff --git a/tests/Dapper.Tests/FakeDbTests.Async.cs b/tests/Dapper.Tests/FakeDbTests.Async.cs new file mode 100644 index 000000000..0a2b5a3c2 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Async.cs @@ -0,0 +1,152 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbAsyncTests + { + [Fact] + public async Task QueryAsync_MapsColumnsToProperties() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var result = (await conn.QueryAsync("SELECT Id, Name FROM Users")).ToList(); + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal("Alice", result[0].Name); + } + + [Fact] + public async Task QueryAsync_ReturnsMultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + var result = (await conn.QueryAsync("SELECT Id, Name FROM Users")).ToList(); + + Assert.Equal(2, result.Count); + } + + [Fact] + public async Task QueryFirstAsync_ReturnsFirstRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "First" } }, + new Dictionary { { "Id", 6 }, { "Name", "Second" } }, + }); + conn.Open(); + + var result = await conn.QueryFirstAsync("SELECT Id, Name FROM Users"); + + Assert.Equal(5, result.Id); + } + + [Fact] + public async Task QueryFirstAsync_ThrowsOnEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + await Assert.ThrowsAsync(() => + conn.QueryFirstAsync("SELECT Id, Name FROM Users")); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var result = await conn.QueryFirstOrDefaultAsync("SELECT Id, Name FROM Users"); + + Assert.Null(result); + } + + [Fact] + public async Task QuerySingleAsync_ReturnsRow_WhenExactlyOne() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Solo" } } + }); + conn.Open(); + + var result = await conn.QuerySingleAsync("SELECT Id, Name FROM Users WHERE Id = 7"); + + Assert.Equal(7, result.Id); + } + + [Fact] + public async Task QuerySingleAsync_ThrowsOnMultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + await Assert.ThrowsAsync(() => + conn.QuerySingleAsync("SELECT Id, Name FROM Users")); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var result = await conn.QuerySingleOrDefaultAsync("SELECT Id, Name FROM Users"); + + Assert.Null(result); + } + + [Fact] + public async Task ExecuteAsync_ReturnsAffectedRowCount() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(5); + conn.Open(); + + var rows = await conn.ExecuteAsync("DELETE FROM Users"); + + Assert.Equal(5, rows); + } + + [Fact] + public async Task ExecuteScalarAsync_ReturnsPreloadedValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(99L); + conn.Open(); + + var count = await conn.ExecuteScalarAsync("SELECT COUNT(*) FROM Users"); + + Assert.Equal(99L, count); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.AsyncCommandDef.cs b/tests/Dapper.Tests/FakeDbTests.AsyncCommandDef.cs new file mode 100644 index 000000000..b5146e119 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.AsyncCommandDef.cs @@ -0,0 +1,295 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for async query overloads that take CommandDefinition (dynamic and generic), + /// and for GetRowParser/GetRowParser(Type) with DbDataReader and IDataReader. + /// + public class FakeDbAsyncCommandDefTests + { + private class User { public int Id { get; set; } public string? Name { get; set; } } + + // ── QueryAsync(CommandDefinition) dynamic overloads ─────────── + + [Fact] + public async Task QueryAsync_Dynamic_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var results = (await conn.QueryAsync(cmd)).ToList(); + + Assert.Single(results); + Assert.Equal(1, (int)results[0].Id); + } + + [Fact] + public async Task QueryFirstAsync_Dynamic_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 2 }, { "Name", "Bob" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + dynamic row = await conn.QueryFirstAsync(cmd); + + Assert.Equal(2, (int)row.Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_Dynamic_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Carol" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + dynamic? row = await conn.QueryFirstOrDefaultAsync(cmd); + + Assert.NotNull(row); + Assert.Equal(3, (int)row!.Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_Dynamic_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = await conn.QueryFirstOrDefaultAsync(cmd); + + Assert.Null(row); + } + + [Fact] + public async Task QuerySingleAsync_Dynamic_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 4 }, { "Name", "Dave" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + dynamic row = await conn.QuerySingleAsync(cmd); + + Assert.Equal(4, (int)row.Id); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_Dynamic_CommandDefinition_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = await conn.QuerySingleOrDefaultAsync(cmd); + + Assert.Null(row); + } + + // ── QueryAsync(string, ...) dynamic overloads ───────────────── + + [Fact] + public async Task QueryAsync_Dynamic_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 } }, + new Dictionary { { "Id", 6 } }, + }); + conn.Open(); + + var results = (await conn.QueryAsync("SELECT Id FROM T")).ToList(); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task QueryFirstAsync_Dynamic_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 } } + }); + conn.Open(); + + dynamic row = await conn.QueryFirstAsync("SELECT Id FROM T"); + Assert.Equal(7, (int)row.Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_Dynamic_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 8 } } + }); + conn.Open(); + + dynamic? row = await conn.QueryFirstOrDefaultAsync("SELECT Id FROM T"); + Assert.NotNull(row); + } + + [Fact] + public async Task QuerySingleAsync_Dynamic_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 9 } } + }); + conn.Open(); + + dynamic row = await conn.QuerySingleAsync("SELECT Id FROM T"); + Assert.Equal(9, (int)row.Id); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_Dynamic_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 10 } } + }); + conn.Open(); + + dynamic? row = await conn.QuerySingleOrDefaultAsync("SELECT Id FROM T"); + Assert.NotNull(row); + } + + // ── Async multimap with Type[] ───────────────────────────────── + + private class Owner { public int Id { get; set; } public string? Name { get; set; } } + private class Pet { public int PetId { get; set; } public string? Breed { get; set; } } + + [Fact] + public async Task QueryAsync_TypeArray_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "PetId", 10 }, { "Breed", "Lab" } + } + }); + conn.Open(); + + var results = (await conn.QueryAsync<(Owner, Pet)>( + "SELECT ...", + new[] { typeof(Owner), typeof(Pet) }, + objs => ((Owner)objs[0], (Pet)objs[1]), + splitOn: "PetId")).ToList(); + + Assert.Single(results); + Assert.Equal("Alice", results[0].Item1.Name); + } + + // ── GetRowParser(IDataReader, Type) ─────────────────────────── + + [Fact] + public void IDataReader_GetRowParser_ByType_Specific_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 11 }, { "Name", "Eve" } } + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var parser = reader.GetRowParser(typeof(User)); + + Assert.True(reader.Read()); + var user = (User)parser(reader); + Assert.Equal(11, user.Id); + } + + [Fact] + public void DbDataReader_GetRowParser_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 12 }, { "Name", "Frank" } } + }); + conn.Open(); + + using DbDataReader dbReader = (DbDataReader)conn.ExecuteReader("SELECT Id, Name FROM Users"); + var parser = dbReader.GetRowParser(typeof(User)); + + Assert.True(dbReader.Read()); + var user = (User)parser(dbReader); + Assert.Equal(12, user.Id); + } + + [Fact] + public void DbDataReader_GetRowParser_ValueType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 99 } } + }); + conn.Open(); + + using DbDataReader dbReader = (DbDataReader)conn.ExecuteReader("SELECT 99"); + // GetRowParser with value type takes a different code path (IsValueType branch) + var parser = dbReader.GetRowParser(); + + Assert.True(dbReader.Read()); + var val = parser(dbReader); + Assert.Equal(99, val); + } + + // ── QueryCachePurged event ──────────────────────────────────── + + [Fact] + public void PurgeQueryCache_FiresEvent_WhenSubscribed() + { + bool fired = false; + SqlMapper.QueryCachePurged += OnPurged; + try + { + SqlMapper.PurgeQueryCache(); + Assert.True(fired); + } + finally + { + SqlMapper.QueryCachePurged -= OnPurged; + } + + void OnPurged(object? sender, EventArgs e) => fired = true; + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.AsyncEnumerable.cs b/tests/Dapper.Tests/FakeDbTests.AsyncEnumerable.cs new file mode 100644 index 000000000..17ce9aac3 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.AsyncEnumerable.cs @@ -0,0 +1,138 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for QueryUnbufferedAsync (IAsyncEnumerable) and related async enumerable paths. + /// + public class FakeDbAsyncEnumerableTests + { + private class User { public int Id { get; set; } public string? Name { get; set; } } + + [Fact] + public async Task QueryUnbufferedAsync_SingleRow_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync("SELECT Id, Name FROM Users")) + { + results.Add(item); + } + + Assert.Single(results); + Assert.Equal(1, results[0].Id); + Assert.Equal("Alice", results[0].Name); + } + + [Fact] + public async Task QueryUnbufferedAsync_MultipleRows_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + new Dictionary { { "Id", 3 }, { "Name", "Carol" } }, + }); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync("SELECT Id, Name FROM Users")) + { + results.Add(item); + } + + Assert.Equal(3, results.Count); + Assert.Equal("Alice", results[0].Name); + Assert.Equal("Bob", results[1].Name); + Assert.Equal("Carol", results[2].Name); + } + + [Fact] + public async Task QueryUnbufferedAsync_EmptyResult_ReturnsNothing() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(System.Array.Empty>()); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync("SELECT Id, Name FROM Users")) + { + results.Add(item); + } + + Assert.Empty(results); + } + + [Fact] + public async Task QueryUnbufferedAsync_WithParameters_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Dave" } } + }); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync( + "SELECT Id, Name FROM Users WHERE Id = @id", new { id = 5 })) + { + results.Add(item); + } + + Assert.Single(results); + Assert.Equal(5, results[0].Id); + } + + [Fact] + public async Task QueryUnbufferedAsync_CollectAll_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync("SELECT Id, Name FROM Users")) + results.Add(item); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task QueryUnbufferedAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Eve" } } + }); + conn.Open(); + + var results = new List(); + await foreach (var item in conn.QueryUnbufferedAsync("SELECT Id, Name FROM T")) + { + results.Add(item); + } + + Assert.Single(results); + Assert.Equal(7, (int)results[0].Id); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.AsyncMultimap.cs b/tests/Dapper.Tests/FakeDbTests.AsyncMultimap.cs new file mode 100644 index 000000000..0132079c3 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.AsyncMultimap.cs @@ -0,0 +1,378 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for async multimap 3/4/5/6/7-type variants (string SQL and CommandDefinition), + /// and QueryAsync/QueryFirstAsync/QuerySingleAsync with Type+CommandDefinition overloads. + /// + public class FakeDbAsyncMultimapTests + { + private class A { public int Id { get; set; } public string? Name { get; set; } } + private class B { public int BId { get; set; } public string? BName { get; set; } } + private class C { public int CId { get; set; } } + private class D { public int DId { get; set; } } + private class E { public int EId { get; set; } } + private class F { public int FId { get; set; } } + private class G { public int GId { get; set; } } + + private static Dictionary MakeRow7() => new() + { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "Brow" }, + { "CId", 3 }, { "DId", 4 }, { "EId", 5 }, { "FId", 6 }, { "GId", 7 } + }; + + // ── QueryAsync(Type, CommandDefinition) ─────────────────────── + + [Fact] + public async Task QueryAsync_Type_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var results = (await conn.QueryAsync(typeof(A), cmd)).Cast().ToList(); + + Assert.Single(results); + Assert.Equal(1, results[0].Id); + } + + [Fact] + public async Task QueryFirstAsync_Type_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 2 }, { "Name", "Bob" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = (A)await conn.QueryFirstAsync(typeof(A), cmd); + + Assert.Equal(2, row.Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_Type_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Carol" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = await conn.QueryFirstOrDefaultAsync(typeof(A), cmd); + + Assert.NotNull(row); + Assert.Equal(3, ((A)row!).Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_Type_CommandDefinition_Empty_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = await conn.QueryFirstOrDefaultAsync(typeof(A), cmd); + + Assert.Null(row); + } + + [Fact] + public async Task QuerySingleAsync_Type_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 4 }, { "Name", "Dave" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = (A)await conn.QuerySingleAsync(typeof(A), cmd); + + Assert.Equal(4, row.Id); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_Type_CommandDefinition_Empty_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); + var row = await conn.QuerySingleOrDefaultAsync(typeof(A), cmd); + + Assert.Null(row); + } + + // ── Async 3-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_3Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 } + } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b, c) => $"{a.Id}-{b.BId}-{c.CId}", + splitOn: "BId,CId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3", results[0]); + } + + // ── Async 4-type string SQL ─────────────────────────────────── + + [Fact] + public async Task QueryAsync_4Types_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 } + } + }); + conn.Open(); + + var results = (await conn.QueryAsync( + "SELECT ...", + (a, b, c, d) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}", + splitOn: "BId,CId,DId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4", results[0]); + } + + // ── Async 4-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_4Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 } + } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b, c, d) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}", + splitOn: "BId,CId,DId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4", results[0]); + } + + // ── Async 5-type string SQL ─────────────────────────────────── + + [Fact] + public async Task QueryAsync_5Types_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 }, { "EId", 5 } + } + }); + conn.Open(); + + var results = (await conn.QueryAsync( + "SELECT ...", + (a, b, c, d, e) => $"{a.Id}-{e.EId}", + splitOn: "BId,CId,DId,EId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-5", results[0]); + } + + // ── Async 5-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_5Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 }, { "EId", 5 } + } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b, c, d, e) => $"{a.Id}-{e.EId}", + splitOn: "BId,CId,DId,EId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-5", results[0]); + } + + // ── Async 6-type string SQL ─────────────────────────────────── + + [Fact] + public async Task QueryAsync_6Types_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 }, { "EId", 5 }, { "FId", 6 } + } + }); + conn.Open(); + + var results = (await conn.QueryAsync( + "SELECT ...", + (a, b, c, d, e, f) => $"{a.Id}-{f.FId}", + splitOn: "BId,CId,DId,EId,FId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-6", results[0]); + } + + // ── Async 6-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_6Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, { "DId", 4 }, { "EId", 5 }, { "FId", 6 } + } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b, c, d, e, f) => $"{a.Id}-{f.FId}", + splitOn: "BId,CId,DId,EId,FId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-6", results[0]); + } + + // ── Async 7-type string SQL ─────────────────────────────────── + + [Fact] + public async Task QueryAsync_7Types_StringSql_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { MakeRow7() }); + conn.Open(); + + var results = (await conn.QueryAsync( + "SELECT ...", + (a, b, c, d, e, f, g) => $"{a.Id}-{g.GId}", + splitOn: "BId,CId,DId,EId,FId,GId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-7", results[0]); + } + + // ── Async 7-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_7Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { MakeRow7() }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b, c, d, e, f, g) => $"{a.Id}-{g.GId}", + splitOn: "BId,CId,DId,EId,FId,GId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-7", results[0]); + } + + // ── Async 2-type CommandDefinition ──────────────────────────── + + [Fact] + public async Task QueryAsync_2Types_CommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "B" } + } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var results = (await conn.QueryAsync( + cmd, + (a, b) => $"{a.Id}-{b.BId}", + splitOn: "BId" + )).ToList(); + + Assert.Single(results); + Assert.Equal("1-2", results[0]); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.AsyncTypeOverloads.cs b/tests/Dapper.Tests/FakeDbTests.AsyncTypeOverloads.cs new file mode 100644 index 000000000..fac0d7b65 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.AsyncTypeOverloads.cs @@ -0,0 +1,255 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for async overloads that take a runtime Type parameter, + /// XElement parameter handling, DataTableHandler.Parse exception, + /// TableValuedParameter.AddParameter, and more SqlMapper coverage. + /// + public class FakeDbAsyncTypeOverloadTests + { + private class User { public int Id { get; set; } public string? Name { get; set; } } + + // ── QueryFirstAsync(Type) ───────────────────────────────────── + + [Fact] + public async Task QueryFirstAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = await conn.QueryFirstAsync(typeof(User), "SELECT Id, Name FROM T"); + Assert.Equal(1, ((User)row).Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 2 }, { "Name", "Bob" } } + }); + conn.Open(); + + var row = await conn.QueryFirstOrDefaultAsync(typeof(User), "SELECT Id, Name FROM T"); + Assert.NotNull(row); + Assert.Equal(2, ((User)row!).Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_ByType_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var row = await conn.QueryFirstOrDefaultAsync(typeof(User), "SELECT Id, Name FROM T"); + Assert.Null(row); + } + + [Fact] + public async Task QuerySingleAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Carol" } } + }); + conn.Open(); + + var row = await conn.QuerySingleAsync(typeof(User), "SELECT Id, Name FROM T"); + Assert.Equal(3, ((User)row).Id); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_ByType_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var row = await conn.QuerySingleOrDefaultAsync(typeof(User), "SELECT Id, Name FROM T"); + Assert.Null(row); + } + + // ── XElement as parameter (covers XElementHandler.SetValue + Format) ── + + [Fact] + public void Execute_WithXElementParam_Works() + { + var element = new XElement("root", new XElement("child", "value")); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // Passing XElement triggers XElementHandler.SetValue and Format + conn.Execute("UPDATE T SET Xml = @xml WHERE Id = 1", new { xml = element }); + } + + [Fact] + public void Query_WithXElementParam_Works() + { + var element = new XElement("filter", "value"); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id FROM T WHERE Xml = @xml", new { xml = element }) + .ToList(); + Assert.Single(result); + } + + // ── DataTableHandler.Parse throws NotImplementedException ───── + + [Fact] + public void DataTableHandler_Parse_Throws_NotImplementedException() + { + // DataTableHandler.Parse is not implemented and throws NotImplementedException + // To trigger it: pass DataTable as target type in Query + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + // This should trigger DataTableHandler.Parse which throws NotImplementedException + Assert.Throws(() => + conn.Query("SELECT Id FROM T").ToList()); + } + + // ── TableValuedParameter.AddParameter ───────────────────────── + + [Fact] + public void TableValuedParameter_AddParameter_Works() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + dt.Rows.Add(1); + dt.Rows.Add(2); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // AsTableValuedParameter() returns ICustomQueryParameter + // When passed to Dapper, it calls AddParameter + var tvp = dt.AsTableValuedParameter(); + var dp = new DynamicParameters(); + dp.Add("ids", tvp); + conn.Execute("EXEC BulkInsert @ids", dp); + } + + // ── CommandDefinition.InferCommandType — StoredProcedure path ─ + + [Fact] + public void CommandDefinition_InferCommandType_StoredProc() + { + // A name with no whitespace/special chars is inferred as StoredProcedure + var cmd = new CommandDefinition("sp_GetUser"); // no whitespace -> StoredProc + Assert.Equal(CommandType.StoredProcedure, cmd.CommandTypeDirect); + } + + [Fact] + public void CommandDefinition_InferCommandType_Text() + { + var cmd = new CommandDefinition("SELECT 1"); // has whitespace -> Text + Assert.Equal(CommandType.Text, cmd.CommandTypeDirect); + } + + // ── CollectCacheGarbage path ─────────────────────────────────── + // Triggered after COLLECT_PER_ITEMS (1000) distinct queries + + // ── DefaultTypeMap additional coverage ──────────────────────── + + private class TypeWithField { public int Id; } + + [Fact] + public void DefaultTypeMap_GetSettableFields_ReturnsFields() + { + var fields = DefaultTypeMap.GetSettableFields(typeof(TypeWithField)); + Assert.NotEmpty(fields); + } + + [Fact] + public void DefaultTypeMap_GetSettableProps_ReturnsProps() + { + var props = DefaultTypeMap.GetSettableProps(typeof(User)); + Assert.Equal(2, props.Count); + } + + // ── Additional SqlMapper coverage: GetCachedSQLCount ────────── + + [Fact] + public void GetCachedSQLCount_ReturnsNonNegative() + { + var count = SqlMapper.GetCachedSQLCount(); + Assert.True(count >= 0); + } + + // ── ExecuteScalarAsync(CommandDefinition) with timeout ───────── + + [Fact] + public async Task ExecuteScalarAsync_WithCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Result", 42 } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT 42", commandTimeout: 5); + var result = await conn.ExecuteScalarAsync(cmd); + Assert.Equal(42, result); + } + + // ── Query with CommandTimeout from Settings ──────────────────── + + [Fact] + public void Query_WithSettingsCommandTimeout_Works() + { + var original = SqlMapper.Settings.CommandTimeout; + try + { + SqlMapper.Settings.CommandTimeout = 30; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + // This exercises the Settings.CommandTimeout fallback in SetupCommand + var cmd = new CommandDefinition("SELECT Id, Name FROM T"); // no explicit timeout + var results = conn.Query(cmd).ToList(); + Assert.Single(results); + } + finally + { + SqlMapper.Settings.CommandTimeout = original; + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.BatchExecute.cs b/tests/Dapper.Tests/FakeDbTests.BatchExecute.cs new file mode 100644 index 000000000..db2b7b097 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.BatchExecute.cs @@ -0,0 +1,219 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbBatchExecuteTests + { + // ── Execute with IEnumerable (sync batch) ────────────────── + + [Fact] + public void Execute_WithList_ExecutesForEach() + { + var items = new[] + { + new { id = 1, name = "Alice" }, + new { id = 2, name = "Bob" }, + new { id = 3, name = "Carol" }, + }; + + using var conn = new fakeDbConnection(new FakeDataStore()); + // Enqueue one result per item + foreach (var _ in items) + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var total = conn.Execute("INSERT INTO Users (Id, Name) VALUES (@id, @name)", items); + + Assert.Equal(3, total); + } + + [Fact] + public void Execute_WithEmptyList_ReturnsZero() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + var total = conn.Execute("INSERT INTO Users VALUES (@id)", + Enumerable.Empty()); + + Assert.Equal(0, total); + } + + [Fact] + public void Execute_WithSingleItem_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var total = conn.Execute("DELETE FROM Users WHERE Id = @id", + new[] { new { id = 42 } }); + + Assert.Equal(1, total); + } + + // ── Async batch execute ─────────────────────────────────────── + + [Fact] + public async Task ExecuteAsync_WithList_ExecutesForEach() + { + var items = new[] + { + new { id = 1 }, + new { id = 2 }, + }; + + using var conn = new fakeDbConnection(new FakeDataStore()); + foreach (var _ in items) + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var total = await conn.ExecuteAsync( + "DELETE FROM Users WHERE Id = @id", items); + + Assert.Equal(2, total); + } + + [Fact] + public async Task ExecuteAsync_Pipelined_WithList_Works() + { + var items = Enumerable.Range(1, 5) + .Select(i => new { id = i }) + .ToList(); + + using var conn = new fakeDbConnection(new FakeDataStore()); + foreach (var _ in items) + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var cmd = new CommandDefinition( + "DELETE FROM Users WHERE Id = @id", + items, + flags: CommandFlags.Pipelined); + + var total = await conn.ExecuteAsync(cmd); + + Assert.Equal(5, total); + } + + [Fact] + public async Task ExecuteAsync_WithEmptyList_ReturnsZero() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + var total = await conn.ExecuteAsync("DELETE FROM Users WHERE Id = @id", + Enumerable.Empty()); + + Assert.Equal(0, total); + } + + // ── ExecuteReaderAsync returning IDataReader (triggers CastResult) ── + + [Fact] + public async Task ExecuteReaderAsync_ViaIDbConnection_ReturnsIDataReader() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + // Call via IDbConnection interface to get Task (triggers Extensions.CastResult) + IDbConnection iconn = conn; + var reader = await iconn.ExecuteReaderAsync("SELECT Id, Name FROM Users"); + using (reader) + { + Assert.True(reader.Read()); + Assert.Equal(1, reader.GetInt32(reader.GetOrdinal("Id"))); + } + } + + [Fact] + public async Task ExecuteReaderAsync_ViaIDbConnection_WithParams_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 } } + }); + conn.Open(); + + IDbConnection iconn = conn; + var reader = await iconn.ExecuteReaderAsync( + "SELECT Id FROM Users WHERE Id = @id", + new { id = 5 }); + using (reader) + { + Assert.True(reader.Read()); + } + } + + // ── DataTable as parameter (triggers DataTableHandler) ──────── + + [Fact] + public void Execute_WithDataTableParameter_Works() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + dt.Rows.Add(1); + dt.Rows.Add(2); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(2); + conn.Open(); + + // Pass DataTable via DynamicParameters (triggers DataTableHandler.SetValue) + var dp = new DynamicParameters(); + dp.Add("ids", dt, DbType.Object); + conn.Execute("EXEC BulkInsert @ids", dp); + } + + // ── Large list (tests padding logic) ───────────────────────── + + [Fact] + public void Query_WithLargeListParameter_Works() + { + var ids = Enumerable.Range(1, 15).ToList(); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(ids.Select(i => + new Dictionary { { "Id", i }, { "Name", $"User{i}" } } + ).ToList()); + conn.Open(); + + var result = conn.Query( + "SELECT Id, Name FROM Users WHERE Id IN @ids", + new { ids }).ToList(); + + Assert.Equal(15, result.Count); + } + + [Fact] + public void Query_WithListParameter_OddCount_Works() + { + var ids = new[] { 1, 2, 3 }; // odd - may trigger padding + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(ids.Select(i => + new Dictionary { { "Id", i }, { "Name", $"U{i}" } } + ).ToList()); + conn.Open(); + + var result = conn.Query( + "SELECT Id, Name FROM Users WHERE Id IN @ids", + new { ids }).ToList(); + + Assert.Equal(3, result.Count); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.CommandDefinition.cs b/tests/Dapper.Tests/FakeDbTests.CommandDefinition.cs new file mode 100644 index 000000000..4cd49d979 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.CommandDefinition.cs @@ -0,0 +1,111 @@ +#if !NET481 +using System; +using System.Data; +using System.Threading; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for CommandDefinition properties and constructor overloads. + /// + public class FakeDbCommandDefinitionTests + { + private class User { public int Id { get; set; } public string? Name { get; set; } } + + [Fact] + public void CommandDefinition_Properties_AreReadable() + { + using var cts = new CancellationTokenSource(); + var cmd = new CommandDefinition( + "SELECT 1", + parameters: new { id = 1 }, + commandTimeout: 30, + commandType: CommandType.Text, + flags: CommandFlags.Buffered, + cancellationToken: cts.Token); + + Assert.Equal("SELECT 1", cmd.CommandText); + Assert.NotNull(cmd.Parameters); + Assert.Equal(30, cmd.CommandTimeout); + Assert.Equal(CommandType.Text, cmd.CommandTypeDirect); + Assert.True(cmd.Buffered); + Assert.False(cmd.Pipelined); + Assert.Equal(CommandFlags.Buffered, cmd.Flags); + Assert.Equal(cts.Token, cmd.CancellationToken); + } + + [Fact] + public void CommandDefinition_StoredProcedure_Type() + { + var cmd = new CommandDefinition("usp_GetUser", + commandType: CommandType.StoredProcedure); + + Assert.Equal(CommandType.StoredProcedure, cmd.CommandTypeDirect); + } + + [Fact] + public void CommandDefinition_Pipelined_Flag() + { + var cmd = new CommandDefinition("SELECT 1", flags: CommandFlags.Pipelined); + + Assert.True(cmd.Pipelined); + Assert.False(cmd.Buffered); + } + + [Fact] + public void CommandDefinition_NoCache_Flag() + { + var cmd = new CommandDefinition("SELECT 1", flags: CommandFlags.NoCache); + + Assert.Equal(CommandFlags.NoCache, cmd.Flags); + Assert.False(cmd.Buffered); + } + + [Fact] + public void CommandDefinition_Transaction_IsPreserved() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + using var tx = conn.BeginTransaction(); + + var cmd = new CommandDefinition("SELECT 1", transaction: tx); + Assert.Equal(tx, cmd.Transaction); + } + + [Fact] + public void CommandDefinition_Execute_WithTimeout_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var cmd = new CommandDefinition("DELETE FROM T WHERE Id = @id", + new { id = 1 }, + commandTimeout: 10, + flags: CommandFlags.Buffered); + + var result = conn.Execute(cmd); + Assert.Equal(1, result); + } + + [Fact] + public void CommandDefinition_NoCache_SkipsCaching() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new System.Collections.Generic.Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM T", + flags: CommandFlags.NoCache | CommandFlags.Buffered); + + var results = System.Linq.Enumerable.ToList(conn.Query(cmd)); + Assert.Single(results); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.CommandDefinitionGetInit.cs b/tests/Dapper.Tests/FakeDbTests.CommandDefinitionGetInit.cs new file mode 100644 index 000000000..69a9111fb --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.CommandDefinitionGetInit.cs @@ -0,0 +1,243 @@ +#if !NET481 +using System; +using System.Data; +using System.Reflection; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers CommandDefinition.GetInit (lines 147-199) and GetBasicPropertySetter (lines 201-209). + /// GetInit generates IL delegates for Oracle-specific command properties (BindByName, + /// InitialLONGFetchSize, FetchSize). GetBasicPropertySetter finds settable properties. + /// + public class FakeDbCommandDefinitionGetInitTests + { + private static Action? CallGetInit(Type? type) + { + CommandDefinition.ResetCommandInitCache(); + var method = typeof(CommandDefinition).GetMethod("GetInit", + BindingFlags.NonPublic | BindingFlags.Static)!; + return (Action?)method.Invoke(null, new object?[] { type }); + } + + // ── L150: null type → returns null (GIGO) ───────────────────── + + [Fact] + public void GetInit_NullType_ReturnsNull() + { + var result = CallGetInit(null); + Assert.Null(result); + } + + // ── No Oracle props → action is null, stored in cache ───────── + + [Fact] + public void GetInit_NoOracleProps_ReturnsNull() + { + var result = CallGetInit(typeof(MinimalFakeCommand)); + Assert.Null(result); + } + + // ── Cache hit: second call same type uses cached value ───────── + + [Fact] + public void GetInit_CacheHit_ReturnsCachedValue() + { + CommandDefinition.ResetCommandInitCache(); + var method = typeof(CommandDefinition).GetMethod("GetInit", + BindingFlags.NonPublic | BindingFlags.Static)!; + + // First call populates cache + var a1 = (Action?)method.Invoke(null, new object?[] { typeof(MinimalFakeCommand) }); + // Second call hits cache (TryGet returns true) + var a2 = (Action?)method.Invoke(null, new object?[] { typeof(MinimalFakeCommand) }); + + Assert.Equal(a1, a2); // both null for a command with no Oracle props + } + + // ── BindByName property → IL generation ─────────────────────── + + [Fact] + public void GetInit_BindByName_GeneratesDelegate_And_SetsTrue() + { + var action = CallGetInit(typeof(FakeCmdWithBindByName)); + Assert.NotNull(action); + + var cmd = new FakeCmdWithBindByName(); + action!(cmd); + Assert.True(cmd.BindByName); + } + + // ── InitialLONGFetchSize property → IL generation ───────────── + + [Fact] + public void GetInit_InitialLONGFetchSize_GeneratesDelegate_And_SetsNegativeOne() + { + var action = CallGetInit(typeof(FakeCmdWithLongFetch)); + Assert.NotNull(action); + + var cmd = new FakeCmdWithLongFetch(); + action!(cmd); + Assert.Equal(-1, cmd.InitialLONGFetchSize); + } + + // ── FetchSize property with FetchSize >= 0 → emits constant ─── + + [Fact] + public void GetInit_FetchSize_NonNegative_GeneratesDelegate_And_SetsFetchSize() + { + var originalFetchSize = SqlMapper.Settings.FetchSize; + try + { + SqlMapper.Settings.FetchSize = 512L; + var action = CallGetInit(typeof(FakeCmdWithFetchSize)); + Assert.NotNull(action); + + var cmd = new FakeCmdWithFetchSize(); + action!(cmd); + Assert.Equal(512L, cmd.FetchSize); + } + finally + { + SqlMapper.Settings.FetchSize = originalFetchSize; + } + } + + // ── FetchSize property with FetchSize < 0 → no emit, but method still built ── + + [Fact] + public void GetInit_FetchSize_Negative_StillBuildsMethod() + { + var originalFetchSize = SqlMapper.Settings.FetchSize; + try + { + SqlMapper.Settings.FetchSize = -1L; + // FetchSize < 0 → the if-block (L184) is false, no IL for FetchSize, + // but method is still built (because FetchSize property exists) + var action = CallGetInit(typeof(FakeCmdWithFetchSize)); + Assert.NotNull(action); // delegate still built (FetchSize prop was found) + + var cmd = new FakeCmdWithFetchSize(); + action!(cmd); // runs the empty method (just Ret) + Assert.Equal(0L, cmd.FetchSize); // unchanged + } + finally + { + SqlMapper.Settings.FetchSize = originalFetchSize; + } + } + + // ── All three Oracle props ───────────────────────────────────── + + [Fact] + public void GetInit_AllOracleProps_SetsAll() + { + var originalFetchSize = SqlMapper.Settings.FetchSize; + try + { + SqlMapper.Settings.FetchSize = 1024L; + var action = CallGetInit(typeof(FakeCmdWithAllOracleProps)); + Assert.NotNull(action); + + var cmd = new FakeCmdWithAllOracleProps(); + action!(cmd); + Assert.True(cmd.BindByName); + Assert.Equal(-1, cmd.InitialLONGFetchSize); + Assert.Equal(1024L, cmd.FetchSize); + } + finally + { + SqlMapper.Settings.FetchSize = originalFetchSize; + } + } + + // ── GetBasicPropertySetter: valid property → returns setter ──── + // Line 205-206: returns prop.GetSetMethod() + + [Fact] + public void GetBasicPropertySetter_ValidProperty_ReturnsSetter() + { + var method = typeof(CommandDefinition).GetMethod("GetBasicPropertySetter", + BindingFlags.NonPublic | BindingFlags.Static)!; + + var result = (MethodInfo?)method.Invoke(null, + new object[] { typeof(FakeCmdWithBindByName), "BindByName", typeof(bool) }); + + Assert.NotNull(result); + } + + // ── GetBasicPropertySetter: wrong type → returns null ───────── + + [Fact] + public void GetBasicPropertySetter_WrongType_ReturnsNull() + { + var method = typeof(CommandDefinition).GetMethod("GetBasicPropertySetter", + BindingFlags.NonPublic | BindingFlags.Static)!; + + var result = (MethodInfo?)method.Invoke(null, + new object[] { typeof(FakeCmdWithBindByName), "BindByName", typeof(int) }); // wrong type + + Assert.Null(result); + } + } + + // ── Minimal IDbCommand base ──────────────────────────────────────── + + internal abstract class MinimalFakeCommandBase : IDbCommand + { + public string CommandText { get; set; } = ""; + public int CommandTimeout { get; set; } + public CommandType CommandType { get; set; } = CommandType.Text; + public IDbConnection? Connection { get; set; } + public IDataParameterCollection Parameters { get; } = new FakeParameterCollection(); + public IDbTransaction? Transaction { get; set; } + public UpdateRowSource UpdatedRowSource { get; set; } + + public void Cancel() { } + public IDbDataParameter CreateParameter() => new MinimalDbParameter2(); + public void Dispose() { } + public int ExecuteNonQuery() => 0; + public IDataReader ExecuteReader() => throw new NotImplementedException(); + public IDataReader ExecuteReader(CommandBehavior behavior) => throw new NotImplementedException(); + public object? ExecuteScalar() => null; + public void Prepare() { } + } + + internal class MinimalFakeCommand : MinimalFakeCommandBase { } + + internal class FakeCmdWithBindByName : MinimalFakeCommandBase + { + public bool BindByName { get; set; } + } + + internal class FakeCmdWithLongFetch : MinimalFakeCommandBase + { + public int InitialLONGFetchSize { get; set; } + } + + internal class FakeCmdWithFetchSize : MinimalFakeCommandBase + { + public long FetchSize { get; set; } + } + + internal class FakeCmdWithAllOracleProps : MinimalFakeCommandBase + { + public bool BindByName { get; set; } + public int InitialLONGFetchSize { get; set; } + public long FetchSize { get; set; } + } + + internal class FakeParameterCollection : System.Collections.ArrayList, IDataParameterCollection + { + public bool Contains(string parameterName) => false; + public int IndexOf(string parameterName) => -1; + public void RemoveAt(string parameterName) { } + public object this[string parameterName] + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DapperRowAdvanced2.cs b/tests/Dapper.Tests/FakeDbTests.DapperRowAdvanced2.cs new file mode 100644 index 000000000..d0434a5d2 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DapperRowAdvanced2.cs @@ -0,0 +1,157 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DapperRow coverage: IReadOnlyDictionary interface, DeadValue paths, + /// ToString with null values, ICollection<KVP>.Add, IDictionary.Add (isAdd path). + /// + public class FakeDbDapperRowAdvanced2Tests + { + private static dynamic GetRow(Dictionary data) + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { data }); + conn.Open(); + return conn.QueryFirst("SELECT * FROM T"); + } + + // ── IReadOnlyDictionary interface ───────────── + + [Fact] + public void DapperRow_IReadOnlyDict_ContainsKey_True() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "Alice" } }); + IReadOnlyDictionary rod = (IReadOnlyDictionary)(object)row; + Assert.True(rod.ContainsKey("Id")); + } + + [Fact] + public void DapperRow_IReadOnlyDict_ContainsKey_False() + { + var row = GetRow(new Dictionary { { "Id", 1 } }); + IReadOnlyDictionary rod = (IReadOnlyDictionary)(object)row; + Assert.False(rod.ContainsKey("Missing")); + } + + [Fact] + public void DapperRow_IReadOnlyDict_Indexer() + { + var row = GetRow(new Dictionary { { "Id", 42 } }); + IReadOnlyDictionary rod = (IReadOnlyDictionary)(object)row; + Assert.Equal(42, rod["Id"]); + } + + [Fact] + public void DapperRow_IReadOnlyDict_Keys() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "Alice" } }); + IReadOnlyDictionary rod = (IReadOnlyDictionary)(object)row; + var keys = rod.Keys.ToList(); + Assert.Contains("Id", keys); + Assert.Contains("Name", keys); + } + + [Fact] + public void DapperRow_IReadOnlyDict_Values() + { + var row = GetRow(new Dictionary { { "Id", 5 } }); + IReadOnlyDictionary rod = (IReadOnlyDictionary)(object)row; + var vals = rod.Values.ToList(); + Assert.Single(vals); + Assert.Equal(5, vals[0]); + } + + [Fact] + public void DapperRow_IReadOnlyCollection_Count() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "A" } }); + IReadOnlyCollection> roc = (IReadOnlyCollection>)(object)row; + Assert.Equal(2, roc.Count); + } + + // ── DeadValue path — after Remove, TryGetValue returns false ── + + [Fact] + public void DapperRow_AfterRemove_TryGetValue_ReturnsFalse() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "Alice" } }); + IDictionary dict = row; + dict.Remove("Name"); + + // TryGetValue on a removed (DeadValue) key should return false (lines 55-57) + Assert.False(dict.TryGetValue("Name", out _)); + } + + [Fact] + public void DapperRow_AfterRemove_ContainsKey_False() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "Alice" } }); + IDictionary dict = row; + dict.Remove("Name"); + Assert.False(dict.ContainsKey("Name")); + } + + // ── ToString with null value ────────────────────────────────── + // fakeDb GetFieldType throws on true null; use Remove to create a null slot + + [Fact] + public void DapperRow_ToString_WithNullValue_ShowsNull() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "Alice" } }); + IDictionary dict = row; + // Set Name to null then check ToString shows NULL + dict["Name"] = null; + string str = ((object)dict).ToString()!; + Assert.Contains("NULL", str); + } + + // ── ICollection.Add ────────────────────────────────────── + + [Fact] + public void DapperRow_ICollection_Add_KVP_Works() + { + var row = GetRow(new Dictionary { { "Id", 1 } }); + ICollection> col = (IDictionary)row; + col.Add(new KeyValuePair("NewKey", 99)); + IDictionary dict = row; + Assert.Equal(99, dict["NewKey"]); + } + + // ── IDictionary.Add(key, value) ─────────────────────────────── + + [Fact] + public void DapperRow_IDictionary_Add_NewKey_Works() + { + var row = GetRow(new Dictionary { { "Id", 1 } }); + IDictionary dict = row; + dict.Add("Extra", "value"); + Assert.Equal("value", dict["Extra"]); + } + + [Fact] + public void DapperRow_IDictionary_Add_DuplicateKey_Throws() + { + var row = GetRow(new Dictionary { { "Id", 1 } }); + IDictionary dict = row; + Assert.Throws(() => dict.Add("Id", 999)); + } + + // ── IReadOnlyDictionary Count with removed entry ────────────── + + [Fact] + public void DapperRow_IReadOnlyCollection_Count_AfterRemove() + { + var row = GetRow(new Dictionary { { "Id", 1 }, { "Name", "A" } }); + IDictionary dict = row; + dict.Remove("Name"); + IReadOnlyCollection> roc = (IReadOnlyCollection>)dict; + Assert.Equal(1, roc.Count); // only Id remains + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DapperRowDescriptor.cs b/tests/Dapper.Tests/FakeDbTests.DapperRowDescriptor.cs new file mode 100644 index 000000000..0bec55e8b --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DapperRowDescriptor.cs @@ -0,0 +1,265 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers SqlMapper.DapperRow.Descriptor.cs (all 86 lines): + /// DapperRowTypeDescriptionProvider, DapperRowTypeDescriptor (ICustomTypeDescriptor), + /// and RowBoundPropertyDescriptor via System.ComponentModel.TypeDescriptor API. + /// + public class FakeDbDapperRowDescriptorTests + { + private static (dynamic row, object obj) GetRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 42 }, { "Name", "Alice" } } + }); + conn.Open(); + dynamic row = conn.QueryFirst("SELECT Id, Name FROM T"); + return (row, (object)row); + } + + // ── TypeDescriptor.GetProperties — exercises GetTypeDescriptor + GetProperties ── + + [Fact] + public void DapperRow_TypeDescriptor_GetProperties_ReturnsColumns() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.True(props.Count >= 2); + Assert.NotNull(props["Id"]); + Assert.NotNull(props["Name"]); + } + + // ── GetProperties(Attribute[]) overload ─────────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetProperties_WithAttributes_Works() + { + var (_, obj) = GetRow(); + // null attributes triggers the same path + var props = TypeDescriptor.GetProperties(obj, (Attribute[]?)null); + Assert.True(props.Count >= 2); + } + + // ── RowBoundPropertyDescriptor.GetValue ─────────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_GetValue_ReturnsCorrectValue() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var idProp = props["Id"]!; + Assert.Equal(42, idProp.GetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.GetValue — missing key returns DBNull ── + + [Fact] + public void DapperRow_PropertyDescriptor_GetValue_AfterRemove_ReturnsDBNull() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var nameProp = props["Name"]!; + nameProp.ResetValue(obj); // removes the entry + Assert.Equal(DBNull.Value, nameProp.GetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.SetValue ─────────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_SetValue_UpdatesValue() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var idProp = props["Id"]!; + idProp.SetValue(obj, 999); + Assert.Equal(999, idProp.GetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.SetValue with DBNull → null ──── + + [Fact] + public void DapperRow_PropertyDescriptor_SetValue_DBNull_SetsNull() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var nameProp = props["Name"]!; + nameProp.SetValue(obj, DBNull.Value); + // GetValue returns DBNull when value is null + Assert.Equal(DBNull.Value, nameProp.GetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.CanResetValue ────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_CanResetValue_ReturnsTrue() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.True(props["Id"]!.CanResetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.ResetValue ───────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_ResetValue_RemovesEntry() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var nameProp = props["Name"]!; + nameProp.ResetValue(obj); + Assert.Equal(DBNull.Value, nameProp.GetValue(obj)); + } + + // ── RowBoundPropertyDescriptor.ShouldSerializeValue ─────────── + + [Fact] + public void DapperRow_PropertyDescriptor_ShouldSerializeValue_True_WhenExists() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.True(props["Id"]!.ShouldSerializeValue(obj)); + } + + [Fact] + public void DapperRow_PropertyDescriptor_ShouldSerializeValue_False_AfterRemove() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + var nameProp = props["Name"]!; + nameProp.ResetValue(obj); + Assert.False(nameProp.ShouldSerializeValue(obj)); + } + + // ── RowBoundPropertyDescriptor.IsReadOnly ───────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_IsReadOnly_False() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.False(props["Id"]!.IsReadOnly); + } + + // ── RowBoundPropertyDescriptor.ComponentType ────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_ComponentType_IsDapperRow() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.Equal("DapperRow", props["Id"]!.ComponentType.Name); + } + + // ── RowBoundPropertyDescriptor.PropertyType ─────────────────── + + [Fact] + public void DapperRow_PropertyDescriptor_PropertyType_ReflectsValueType() + { + var (_, obj) = GetRow(); + var props = TypeDescriptor.GetProperties(obj); + Assert.Equal(typeof(int), props["Id"]!.PropertyType); + } + + // ── ICustomTypeDescriptor.GetAttributes ─────────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetAttributes_ReturnsEmpty() + { + var (_, obj) = GetRow(); + var attrs = TypeDescriptor.GetAttributes(obj); + Assert.NotNull(attrs); + } + + // ── ICustomTypeDescriptor.GetClassName ──────────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetClassName_ReturnsName() + { + var (_, obj) = GetRow(); + var name = TypeDescriptor.GetClassName(obj); + Assert.NotNull(name); + Assert.Contains("DapperRow", name); + } + + // ── ICustomTypeDescriptor.GetComponentName ──────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetComponentName_ReturnsNull() + { + var (_, obj) = GetRow(); + // GetComponentName returns null for DapperRow + var name = TypeDescriptor.GetComponentName(obj); + // null is acceptable + } + + // ── ICustomTypeDescriptor.GetConverter ──────────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetConverter_ReturnsExpandable() + { + var (_, obj) = GetRow(); + var conv = TypeDescriptor.GetConverter(obj); + Assert.NotNull(conv); + Assert.IsType(conv); + } + + // ── ICustomTypeDescriptor.GetDefaultEvent ───────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetDefaultEvent_ReturnsNull() + { + var (_, obj) = GetRow(); + var ev = TypeDescriptor.GetDefaultEvent(obj); + Assert.Null(ev); + } + + // ── ICustomTypeDescriptor.GetDefaultProperty ────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetDefaultProperty_ReturnsNull() + { + var (_, obj) = GetRow(); + var prop = TypeDescriptor.GetDefaultProperty(obj); + Assert.Null(prop); + } + + // ── ICustomTypeDescriptor.GetEvents ─────────────────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetEvents_ReturnsEmpty() + { + var (_, obj) = GetRow(); + var events = TypeDescriptor.GetEvents(obj); + Assert.Equal(0, events.Count); + } + + [Fact] + public void DapperRow_TypeDescriptor_GetEventsWithAttributes_ReturnsEmpty() + { + var (_, obj) = GetRow(); + var events = TypeDescriptor.GetEvents(obj, (Attribute[]?)null); + Assert.Equal(0, events.Count); + } + + // ── GetExtendedTypeDescriptor via TypeDescriptor ─────────────── + + [Fact] + public void DapperRow_TypeDescriptor_GetEditor_ReturnsNull() + { + var (_, obj) = GetRow(); + // GetEditor returns null for DapperRow + var editor = TypeDescriptor.GetEditor(obj, typeof(object)); + Assert.Null(editor); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DapperRowMetaObject2.cs b/tests/Dapper.Tests/FakeDbTests.DapperRowMetaObject2.cs new file mode 100644 index 000000000..b4a9cd7c3 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DapperRowMetaObject2.cs @@ -0,0 +1,76 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DapperRowMetaObject coverage: lines 26-28 (constructor with value), + /// lines 93-96 (BindInvokeMember). + /// + public class FakeDbDapperRowMetaObject2Tests + { + private static dynamic GetRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + return conn.QueryFirst("SELECT * FROM T"); + } + + // ── BindGetMember on a missing property → null ───────────────── + // DapperRowMetaObject.BindGetMember (already covered) + + [Fact] + public void DapperRow_Dynamic_GetMissing_ReturnsNull() + { + dynamic row = GetRow(); + var val = row.NonExistentProp; + Assert.Null(val); + } + + // ── BindSetMember sets value ────────────────────────────────── + + [Fact] + public void DapperRow_Dynamic_SetMember_Works() + { + dynamic row = GetRow(); + row.Name = "Updated"; + Assert.Equal("Updated", (string)row.Name); + } + + // ── BindInvokeMember — calling a method on the dynamic row ───── + // BindInvokeMember redirects to GetValue(binder.Name), so calling any method + // returns GetValue of that method name (null if not a key). + + [Fact] + public void DapperRow_Dynamic_InvokeMember_ReturnsGetValue() + { + dynamic row = GetRow(); + // Calling any method via dynamic dispatch hits BindInvokeMember, + // which calls GetValue("SomeMethod") -> returns null (no such key) + var result = row.NonExistentMethod(); + Assert.Null(result); + } + + // ── GetDynamicMemberNames (lines 92-96) ─────────────────────── + + [Fact] + public void DapperRow_GetDynamicMemberNames_ReturnsMemberNames() + { + dynamic row = GetRow(); + var provider = (System.Dynamic.IDynamicMetaObjectProvider)row; + var expr = System.Linq.Expressions.Expression.Constant((object)row); + var meta = provider.GetMetaObject(expr); + var names = meta.GetDynamicMemberNames().ToList(); + Assert.Contains("Id", names); + Assert.Contains("Name", names); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DataTableParameter.cs b/tests/Dapper.Tests/FakeDbTests.DataTableParameter.cs new file mode 100644 index 000000000..37d32b970 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DataTableParameter.cs @@ -0,0 +1,100 @@ +#if !NET481 +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests that exercise DataTableHandler by passing DataTable as anonymous object parameter + /// (not via DynamicParameters.Add which bypasses the type handler). + /// + public class FakeDbDataTableParameterTests + { + // ── DataTable as anonymous object parameter ─────────────────── + // This triggers DataTableHandler.SetValue via the type handler lookup. + + [Fact] + public void Execute_DataTable_AsAnonymousParam_CallsTypeHandler() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + dt.Rows.Add(1); + dt.Rows.Add(2); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(2); + conn.Open(); + + // Pass DataTable as an anonymous object property — triggers DataTableHandler.SetValue + conn.Execute("EXEC BulkInsert @ids", new { ids = dt }); + } + + [Fact] + public void Query_DataTable_AsAnonymousParam_Works() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + dt.Rows.Add(1); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + // Triggers DataTableHandler.SetValue when expanding parameters + var result = conn.Query( + "SELECT Id FROM T WHERE Id IN @ids", new { ids = dt }).ToList(); + + Assert.Single(result); + } + + // ── AsTableValuedParameter ──────────────────────────────────── + + [Fact] + public void DataTable_AsTableValuedParameter_Constructor() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + dt.Rows.Add(1); + + // Exercise the TableValuedParameter constructor paths + var tvp = dt.AsTableValuedParameter(); + Assert.NotNull(tvp); + } + + [Fact] + public void DataTable_AsTableValuedParameter_WithTypeName() + { + var dt = new DataTable(); + dt.Columns.Add("Id", typeof(int)); + + var tvp = dt.AsTableValuedParameter("dbo.IdList"); + Assert.NotNull(tvp); + } + + // ── DataTable.SetTypeName / GetTypeName ─────────────────────── + + [Fact] + public void DataTable_SetTypeName_GetTypeName_Works() + { + var dt = new DataTable(); + dt.SetTypeName("dbo.MyType"); + Assert.Equal("dbo.MyType", dt.GetTypeName()); + } + + [Fact] + public void DataTable_SetTypeName_Null_ClearsTypeName() + { + var dt = new DataTable(); + dt.SetTypeName("dbo.MyType"); + dt.SetTypeName(null!); + Assert.Null(dt.GetTypeName()); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DbString.cs b/tests/Dapper.Tests/FakeDbTests.DbString.cs new file mode 100644 index 000000000..e78604814 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DbString.cs @@ -0,0 +1,134 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbDbStringTests + { + // ── DbString construction ────────────────────────────────────── + + [Fact] + public void DbString_DefaultsToUnicode() + { + var s = new DbString { Value = "hello" }; + Assert.False(s.IsAnsi); + } + + [Fact] + public void DbString_ToString_ContainsValue() + { + var s = new DbString { Value = "world", IsAnsi = true, Length = 50 }; + var str = s.ToString(); + Assert.NotNull(str); + Assert.NotEmpty(str); + } + + [Fact] + public void DbString_FixedLength_WithoutLength_ThrowsOnAddParameter() + { + var s = new DbString { Value = "x", IsFixedLength = true }; // Length stays -1 + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + // DbString.AddParameter throws InvalidOperationException for fixed-length + length=-1 + Assert.Throws(() => + conn.Execute("SELECT @p", new { p = s })); + } + + [Fact] + public void DbString_AnsiVarChar_PassedAsParameter() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", "result" } } + }); + conn.Open(); + + var s = new DbString { Value = "search", IsAnsi = true, Length = 100 }; + var result = conn.QueryFirst("SELECT @p AS Val", new { p = s }); + Assert.Equal("result", result); + } + + [Fact] + public void DbString_UnicodeFixedLength_PassedAsParameter() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", "result" } } + }); + conn.Open(); + + var s = new DbString { Value = "test", IsAnsi = false, IsFixedLength = true, Length = 50 }; + var result = conn.QueryFirst("SELECT @p AS Val", new { p = s }); + Assert.Equal("result", result); + } + + [Fact] + public void DbString_AnsiFixedLength_PassedAsParameter() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", "ok" } } + }); + conn.Open(); + + var s = new DbString { Value = "test", IsAnsi = true, IsFixedLength = true, Length = 10 }; + var result = conn.QueryFirst("SELECT @p AS Val", new { p = s }); + Assert.Equal("ok", result); + } + + [Fact] + public void DbString_NullValue_PassedAsParameter() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var s = new DbString { Value = null, IsAnsi = false }; + conn.Execute("UPDATE T SET Col = @p", new { p = s }); + } + + [Fact] + public void DbString_LongString_AutoSizesToMax() + { + // String longer than DefaultLength (4000) should not truncate + var longVal = new string('x', 5000); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + var s = new DbString { Value = longVal, IsAnsi = false }; + conn.Execute("UPDATE T SET Col = @p", new { p = s }); + } + + // ── DbString used inline in query ───────────────────────────── + + [Fact] + public void Query_WithDbStringParameter_Executes() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var nameParam = new DbString { Value = "Alice", IsAnsi = false, Length = 50 }; + var result = conn.Query( + "SELECT Id, Name FROM Users WHERE Name = @name", + new { name = nameParam }).ToList(); + + Assert.Single(result); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DbStringAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.DbStringAdvanced.cs new file mode 100644 index 000000000..ef3f882b3 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DbStringAdvanced.cs @@ -0,0 +1,128 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DbString coverage: value+length constructor, parameter reuse branch, + /// fixed-length modes, and AddParameter with existing parameter. + /// + public class FakeDbDbStringAdvancedTests + { + // ── DbString(value, length) constructor ────────────────────── + + [Fact] + public void DbString_ValueLength_Constructor_Works() + { + var s = new DbString("hello", 50); + Assert.Equal("hello", s.Value); + Assert.Equal(50, s.Length); + // IsAnsi defaults to IsAnsiDefault + Assert.Equal(DbString.IsAnsiDefault, s.IsAnsi); + } + + [Fact] + public void DbString_ValueLength_NullValue_Works() + { + var s = new DbString(null, 100); + Assert.Null(s.Value); + Assert.Equal(100, s.Length); + } + + // ── DbString AddParameter with IsFixedLength + length specified ─ + + [Fact] + public void DbString_FixedLength_AddParameter_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // IsFixedLength requires Length to be set + var dp = new DynamicParameters(); + dp.Add("name", new DbString { Value = "hi", IsFixedLength = true, Length = 10, IsAnsi = false }); + conn.Execute("UPDATE T SET Name = @name", dp); + } + + [Fact] + public void DbString_AnsiFixedLength_AddParameter_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var dp = new DynamicParameters(); + dp.Add("name", new DbString { Value = "hi", IsFixedLength = true, Length = 10, IsAnsi = true }); + conn.Execute("UPDATE T SET Name = @name", dp); + } + + [Fact] + public void DbString_IsFixedLength_NoLength_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // Length == -1 and IsFixedLength == true should throw + var dp = new DynamicParameters(); + dp.Add("name", new DbString { Value = "hi", IsFixedLength = true, Length = -1 }); + Assert.Throws(() => + conn.Execute("UPDATE T SET Name = @name", dp)); + } + + [Fact] + public void DbString_LongValue_SetsExplicitLength() + { + // When Length == -1 and Value.Length > DefaultLength, Size = Length (=-1, meaning max) + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var longValue = new string('x', DbString.DefaultLength + 1); + var dp = new DynamicParameters(); + dp.Add("name", new DbString { Value = longValue, Length = -1, IsAnsi = false }); + conn.Execute("UPDATE T SET Name = @name", dp); + } + + // ── DbString via Query ──────────────────────────────────────── + + [Fact] + public void Query_WithDbString_Param_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id FROM T WHERE Name = @name", + new { name = new DbString { Value = "Alice", IsAnsi = true } }).ToList(); + Assert.Single(result); + } + + // ── IsAnsiDefault static property ──────────────────────────── + + [Fact] + public void DbString_IsAnsiDefault_CanBeChanged() + { + var original = DbString.IsAnsiDefault; + try + { + DbString.IsAnsiDefault = !original; + var s = new DbString("test"); + Assert.Equal(!original, s.IsAnsi); + } + finally + { + DbString.IsAnsiDefault = original; + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DbWrappedReader.cs b/tests/Dapper.Tests/FakeDbTests.DbWrappedReader.cs new file mode 100644 index 000000000..477e50976 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DbWrappedReader.cs @@ -0,0 +1,437 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Targeted tests for DbWrappedReader — the DbDataReader wrapper returned by ExecuteReader. + /// Exercises the many delegate methods to improve coverage. + /// + public class FakeDbDbWrappedReaderTests + { + private static fakeDbConnection CreateOpenConnection(IEnumerable> rows) + { + var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(rows); + conn.Open(); + return conn; + } + + [Fact] + public void DbWrappedReader_HasRows_IsTrue_WhenDataPresent() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + Assert.True(reader.HasRows); + } + + [Fact] + public void DbWrappedReader_IsClosed_False_BeforeClose() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + Assert.False(reader.IsClosed); + } + + [Fact] + public void DbWrappedReader_RecordsAffected_ReturnsValue() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + // RecordsAffected is valid for SELECT queries (typically -1 or 0) + Assert.True(reader.RecordsAffected >= -1); + } + + [Fact] + public void DbWrappedReader_Depth_ReturnsValue() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + Assert.True(reader.Depth >= 0); + } + + [Fact] + public void DbWrappedReader_VisibleFieldCount_EqualsFieldCount() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT A, B FROM T"); + Assert.Equal(reader.FieldCount, reader.VisibleFieldCount); + } + + [Fact] + public void DbWrappedReader_GetBoolean_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Flag", true } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Flag FROM T"); + reader.Read(); + Assert.True(reader.GetBoolean(0)); + } + + [Fact] + public void DbWrappedReader_GetInt16_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", (short)42 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal((short)42, reader.GetInt16(0)); + } + + [Fact] + public void DbWrappedReader_GetInt64_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 9999999999L } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(9999999999L, reader.GetInt64(0)); + } + + [Fact] + public void DbWrappedReader_GetFloat_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 3.14f } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(3.14f, reader.GetFloat(0)); + } + + [Fact] + public void DbWrappedReader_GetDouble_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 2.718 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(2.718, reader.GetDouble(0)); + } + + [Fact] + public void DbWrappedReader_GetDecimal_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 1.23m } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(1.23m, reader.GetDecimal(0)); + } + + [Fact] + public void DbWrappedReader_GetDateTime_Works() + { + var dt = new DateTime(2024, 1, 1); + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", dt } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(dt, reader.GetDateTime(0)); + } + + [Fact] + public void DbWrappedReader_GetGuid_Works() + { + var guid = Guid.NewGuid(); + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", guid } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(guid, reader.GetGuid(0)); + } + + [Fact] + public void DbWrappedReader_GetString_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", "hello" } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal("hello", reader.GetString(0)); + } + + [Fact] + public void DbWrappedReader_GetFieldValue_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 99 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(99, reader.GetFieldValue(0)); + } + + [Fact] + public async Task DbWrappedReader_GetFieldValueAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 77 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + var val = await reader.GetFieldValueAsync(0); + Assert.Equal(77, val); + } + + [Fact] + public async Task DbWrappedReader_ReadAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } }, + new Dictionary { { "Id", 2 } }, + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + Assert.True(await reader.ReadAsync()); + Assert.Equal(1, reader.GetInt32(0)); + Assert.True(await reader.ReadAsync()); + Assert.Equal(2, reader.GetInt32(0)); + Assert.False(await reader.ReadAsync()); + } + + [Fact] + public async Task DbWrappedReader_IsDBNullAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", DBNull.Value } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT NULL AS Val"); + reader.Read(); + Assert.True(await reader.IsDBNullAsync(0)); + } + + [Fact] + public async Task DbWrappedReader_NextResultAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + // NextResultAsync returns false when there's only one result set + var hasMore = await reader.NextResultAsync(); + Assert.False(hasMore); + } + + [Fact] + public void DbWrappedReader_GetSchemaTable_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + // May return null or a DataTable — just verify it doesn't throw + var schema = reader.GetSchemaTable(); + // schema may be null for fakeDb + } + + [Fact] + public void DbWrappedReader_GetDataTypeName_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 42 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + var typeName = reader.GetDataTypeName(0); + Assert.NotNull(typeName); + } + + [Fact] + public void DbWrappedReader_GetFieldType_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 42 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + var type = reader.GetFieldType(0); + Assert.NotNull(type); + } + + [Fact] + public void DbWrappedReader_GetValues_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "A", 1 }, { "B", "hello" } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT A, B FROM T"); + reader.Read(); + var values = new object[2]; + var count = reader.GetValues(values); + Assert.Equal(2, count); + } + + [Fact] + public void DbWrappedReader_Indexer_ByOrdinal_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 55 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal(55, reader[0]); + } + + [Fact] + public void DbWrappedReader_Indexer_ByName_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", "test" } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + Assert.Equal("test", reader["Val"]); + } + + [Fact] + public async Task DbWrappedReader_CloseAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + await reader.CloseAsync(); + } + + [Fact] + public async Task DbWrappedReader_DisposeAsync_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + await reader.DisposeAsync(); + } + + [Fact] + public void DbWrappedReader_NextResult_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Id", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + // After all rows, NextResult should return false (single result set) + Assert.False(reader.NextResult()); + } + + [Fact] + public void DbWrappedReader_GetProviderSpecificFieldType_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 1 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + var type = reader.GetProviderSpecificFieldType(0); + Assert.NotNull(type); + } + + [Fact] + public void DbWrappedReader_GetProviderSpecificValue_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "Val", 123 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Val FROM T"); + reader.Read(); + var val = reader.GetProviderSpecificValue(0); + Assert.NotNull(val); + } + + [Fact] + public void DbWrappedReader_GetProviderSpecificValues_Works() + { + using var conn = CreateOpenConnection(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 } } + }); + + using var reader = (DbDataReader)conn.ExecuteReader("SELECT A, B FROM T"); + reader.Read(); + var values = new object[2]; + reader.GetProviderSpecificValues(values); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapAdvanced.cs new file mode 100644 index 000000000..149c14e3e --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapAdvanced.cs @@ -0,0 +1,195 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Reflection; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DefaultTypeMap tests: base-class property setter, FindConstructor with matching params, + /// GetMember underscore paths, and MatchNamesWithUnderscores deep matching. + /// + public class FakeDbDefaultTypeMapAdvancedTests + { + // ── GetPropertySetter — property from base class ─────────────── + + private abstract class Base { public int BaseId { get; set; } } + private class Derived : Base { public string? Name { get; set; } } + + [Fact] + public void GetPropertySetter_BaseClassProperty_Works() + { + // GetPropertySetter takes different path when DeclaringType != type + var prop = typeof(Derived).GetProperty("BaseId")!; + // BaseId is declared on Base, not Derived + Assert.Equal(typeof(Base), prop.DeclaringType); + + var setter = DefaultTypeMap.GetPropertySetter(prop, typeof(Derived)); + Assert.NotNull(setter); + } + + [Fact] + public void GetPropertySetterOrThrow_BaseClassProperty_Works() + { + var prop = typeof(Derived).GetProperty("BaseId")!; + var setter = DefaultTypeMap.GetPropertySetterOrThrow(prop, typeof(Derived)); + Assert.NotNull(setter); + } + + // ── FindConstructor — with parameter names/types ─────────────── + // Must use a class WITHOUT a parameterless ctor, because FindConstructor + // returns the no-arg ctor first when it exists. + + private class NoDefaultCtor + { + public int Id { get; } + public string? Name { get; } + public NoDefaultCtor(int id) { Id = id; } + public NoDefaultCtor(int id, string name) { Id = id; Name = name; } + } + + [Fact] + public void FindConstructor_WithParams_Matches() + { + var map = new DefaultTypeMap(typeof(NoDefaultCtor)); + var ctor = map.FindConstructor(new[] { "id", "name" }, new[] { typeof(int), typeof(string) }); + Assert.NotNull(ctor); + Assert.Equal(2, ctor!.GetParameters().Length); + } + + [Fact] + public void FindConstructor_SingleParam_Matches() + { + var map = new DefaultTypeMap(typeof(NoDefaultCtor)); + var ctor = map.FindConstructor(new[] { "id" }, new[] { typeof(int) }); + Assert.NotNull(ctor); + Assert.Equal(1, ctor!.GetParameters().Length); + } + + [Fact] + public void FindConstructor_NoMatch_ReturnsNull() + { + var map = new DefaultTypeMap(typeof(NoDefaultCtor)); + // No constructor matching (double) type + var ctor = map.FindConstructor(new[] { "nonexistent" }, new[] { typeof(double) }); + Assert.Null(ctor); + } + + // ── GetConstructorParameter — throw path ─────────────────────── + + [Fact] + public void GetConstructorParameter_MissingName_Throws() + { + var map = new DefaultTypeMap(typeof(NoDefaultCtor)); + var ctor = map.FindConstructor(new[] { "id" }, new[] { typeof(int) })!; + Assert.Throws(() => map.GetConstructorParameter(ctor, "nonexistent")); + } + + // ── GetMember — underscore field matching ────────────────────── + + private class WithUnderscoreField + { + public int user_id = 0; + public string? first_name = null; + } + + [Fact] + public void GetMember_Underscore_Field_ExactMatch() + { + var map = new DefaultTypeMap(typeof(WithUnderscoreField)); + var member = map.GetMember("user_id"); + Assert.NotNull(member); + } + + [Fact] + public void GetMember_Underscore_Field_WithMatchNamesWithUnderscores() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + // UserId -> user_id (strip underscores from field name to match column) + var map = new DefaultTypeMap(typeof(WithUnderscoreField)); + // Column "user_id" should match field "user_id" exactly (no need for underscore stripping) + var member = map.GetMember("user_id"); + Assert.NotNull(member); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── MatchNamesWithUnderscores — query path ───────────────────── + + private class SnakeUser + { + public int UserId { get; set; } + public string? FirstName { get; set; } + } + + [Fact] + public void DefaultTypeMap_MatchNamesWithUnderscores_Query_CaseInsensitive() + { + // Tests underscore matching in MatchFirstOrDefault (lines 214-240) + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "USER_ID", 42 }, + { "FIRST_NAME", "Grace" } + } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT user_id, first_name FROM T"); + Assert.Equal(42, result.UserId); + Assert.Equal("Grace", result.FirstName); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── FindExplicitConstructor — type with attribute ────────────── + + private class WithExplicitCtor + { + public int Id { get; } + [ExplicitConstructor] + public WithExplicitCtor(int id) { Id = id; } + } + + [Fact] + public void FindExplicitConstructor_WithAttribute_ReturnsIt() + { + var map = new DefaultTypeMap(typeof(WithExplicitCtor)); + var ctor = map.FindExplicitConstructor(); + Assert.NotNull(ctor); + Assert.Equal(1, ctor!.GetParameters().Length); + } + + // ── GetPropertySetter throw ──────────────────────────────────── + + private class GetOnlyProp { public int Val { get; } } + + [Fact] + public void GetPropertySetterOrThrow_NoSetter_Throws() + { + // GetPropertySetterOrThrow throws InvalidOperationException when no setter + var prop = typeof(GetOnlyProp).GetProperty("Val")!; + Assert.Throws(() => + DefaultTypeMap.GetPropertySetterOrThrow(prop, typeof(GetOnlyProp))); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapUnderscore.cs b/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapUnderscore.cs new file mode 100644 index 000000000..c42263a14 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DefaultTypeMapUnderscore.cs @@ -0,0 +1,196 @@ +#if !NET481 +using System.Collections.Generic; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Targets specific uncovered paths in DefaultTypeMap: + /// - MatchFirstOrDefault lines 213 (exact after normalize), 229 (exact normalize both), 236 (case-insensitive normalize both) + /// - FindConstructor line 85: EqualsCIU (underscore match in constructor params) + /// - GetMember field underscore paths (lines 164-173) + /// + public class FakeDbDefaultTypeMapUnderscoreTests + { + // ── MatchFirstOrDefault line 213: normalized column matches property exactly ── + // column "User_Id" → normalized "UserId", property "UserId" → exact ordinal match + + private class UserWithPascal { public int UserId { get; set; } } + + [Fact] + public void MatchNamesWithUnderscores_Line213_ExactNormalizedMatch() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "User_Id", 42 } } + }); + conn.Open(); + + // column "User_Id" → normalized "UserId" → exact match with property "UserId" + var result = conn.QueryFirst("SELECT User_Id FROM T"); + Assert.Equal(42, result.UserId); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── MatchFirstOrDefault line 229: normalized column matches normalized property ── + // Property has underscore: "User_Id", column "UserId" + // → normalized column "UserId", normalized property "UserId" → exact ordinal match + + private class UserWithUnderscoreProp { public int User_Id { get; set; } } + + [Fact] + public void MatchNamesWithUnderscores_Line229_NormalizedBothExact() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "UserId", 7 } } + }); + conn.Open(); + + // column "UserId" (no underscore) → normalized "UserId" + // property "User_Id" → normalized "UserId" → exact ordinal match at line 229 + var result = conn.QueryFirst("SELECT UserId FROM T"); + Assert.Equal(7, result.User_Id); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── MatchFirstOrDefault line 236: case-insensitive normalized both ── + // Property "user_id" (lowercase), column "UserId" (PascalCase) + // → normalized column "UserId", normalized property "userid" → case-insensitive match at line 236 + + private class UserLowercase { public int user_id { get; set; } } + + [Fact] + public void MatchNamesWithUnderscores_Line236_CaseInsensitiveNormalizedBoth() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "UserId", 99 } } + }); + conn.Open(); + + // column "UserId" → normalized "UserId" + // property "user_id" → normalized "userid" + // "UserId" != "userid" (case-sensitive), but case-insensitive → MATCH at line 236 + var result = conn.QueryFirst("SELECT UserId FROM T"); + Assert.Equal(99, result.user_id); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── FindConstructor line 85: EqualsCIU path ──────────────────── + // ctor param "userId", column name "user_id" → EqualsCI fails, EqualsCIU succeeds + + private class WithCtorUnderscore + { + public int UserId { get; } + public WithCtorUnderscore(int userId) { UserId = userId; } + } + + [Fact] + public void FindConstructor_EqualsCIU_WithUnderscoreColumnName() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + var map = new DefaultTypeMap(typeof(WithCtorUnderscore)); + // "user_id" → EqualsCIU("userId", "user_id") → strips underscores → "userid" == "userid" → true + var ctor = map.FindConstructor(new[] { "user_id" }, new[] { typeof(int) }); + Assert.NotNull(ctor); + Assert.Equal(1, ctor!.GetParameters().Length); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + // ── GetMember field with underscore matching (lines 164-173) ─── + // column "user_id" → effective "userid" → matches field "userid" (no underscores) + + private class WithNoUnderscoreField + { + public int userid = 0; + } + + [Fact] + public void GetMember_Field_WithUnderscoreNormalization_DirectTest() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + // Directly test GetMember: column "user_id" → effective "userid" → matches field "userid" + var map = new DefaultTypeMap(typeof(WithNoUnderscoreField)); + var member = map.GetMember("user_id"); + Assert.NotNull(member); + Assert.Equal("userid", member!.Field?.Name); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + [Fact] + public void GetMember_Field_WithUnderscoreNormalization_Query() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + SqlMapper.PurgeQueryCache(); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "user_id", 5 } } + }); + conn.Open(); + + // column "user_id" → effectiveColumnName "userid" → matches field "userid" (exact ordinal) + var result = conn.QueryFirst("SELECT user_id FROM T"); + Assert.Equal(5, result.userid); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + SqlMapper.PurgeQueryCache(); + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DeserializerKeyDebug.cs b/tests/Dapper.Tests/FakeDbTests.DeserializerKeyDebug.cs new file mode 100644 index 000000000..74ca9019f --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DeserializerKeyDebug.cs @@ -0,0 +1,157 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Reflection; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers DeserializerKey.ToString() debug method (lines 93-111 in SqlMapper.TypeDeserializerCache.cs) + /// via reflection. Three paths: + /// - names is not null (copyDown=true): returns string.Join(", ", names) + /// - reader is not null (copyDown=false): iterates reader.GetName + /// - both null (copyDown=false, null reader): returns base.ToString() + /// Also covers DisableCommandBehaviorOptimizations (lines 42-55 in SqlMapper.Settings.cs). + /// + public class FakeDbDeserializerKeyDebugTests + { + private static (Type keyType, ConstructorInfo ctor) GetDeserializerKey() + { + var assembly = typeof(SqlMapper).Assembly; + var cacheType = assembly.GetType("Dapper.SqlMapper+TypeDeserializerCache")!; + var keyType = cacheType.GetNestedType("DeserializerKey", BindingFlags.NonPublic)!; + var ctor = keyType.GetConstructors( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + return (keyType, ctor); + } + + // ── ToString() — names path (copyDown=true) ─────────────────── + + [Fact] + public void DeserializerKey_ToString_NamesPath_ReturnsColumnNames() + { + var (_, ctor) = GetDeserializerKey(); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id, Name FROM T"); + reader.Read(); + + // copyDown=true copies names/types from reader into arrays + var key = ctor.Invoke(new object[] { 42, 0, 2, false, reader, true }); + var str = key!.ToString()!; + + Assert.Contains("Id", str); + Assert.Contains("Name", str); + } + + // ── ToString() — reader path (copyDown=false) ───────────────── + + [Fact] + public void DeserializerKey_ToString_ReaderPath_IteratesReader() + { + var (_, ctor) = GetDeserializerKey(); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Col1", 10 } } + }); + conn.Open(); + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Col1 FROM T"); + reader.Read(); + + // copyDown=false stores reader reference + var key = ctor.Invoke(new object[] { 99, 0, 1, false, reader, false }); + var str = key!.ToString()!; + + Assert.Contains("Col1", str); + } + + // ── ToString() — base path (names=null, reader=null) ────────── + + [Fact] + public void DeserializerKey_ToString_BasePath_ReturnsNonNull() + { + var (_, ctor) = GetDeserializerKey(); + + // copyDown=false with null reader → both names and reader are null + var key = ctor.Invoke(new object?[] { 0, 0, 0, false, null!, false }); + var str = key!.ToString(); + + Assert.NotNull(str); // base.ToString() returns the type name + } + + // ── DisableCommandBehaviorOptimizations (lines 42-55) ───────── + + [Fact] + public void Settings_DisableCommandBehaviorOptimizations_SingleResult_ReturnsTrue() + { + var method = typeof(SqlMapper.Settings).GetMethod( + "DisableCommandBehaviorOptimizations", + BindingFlags.NonPublic | BindingFlags.Static)!; + + // Ensure we're at defaults so the method isn't already a no-op + SqlMapper.Settings.SetDefaults(); + var originalSingle = SqlMapper.Settings.UseSingleResultOptimization; + var originalRow = SqlMapper.Settings.UseSingleRowOptimization; + + try + { + // Exception message mentioning SingleResult triggers the disable + var ex = new Exception("CommandBehavior.SingleResult is not supported by this provider"); + var result = (bool)method.Invoke(null, new object[] + { + System.Data.CommandBehavior.SingleResult, + ex + })!; + + Assert.True(result); + Assert.False(SqlMapper.Settings.UseSingleResultOptimization); + } + finally + { + SqlMapper.Settings.UseSingleResultOptimization = originalSingle; + SqlMapper.Settings.UseSingleRowOptimization = originalRow; + } + } + + [Fact] + public void Settings_DisableCommandBehaviorOptimizations_NoMatch_ReturnsFalse() + { + var method = typeof(SqlMapper.Settings).GetMethod( + "DisableCommandBehaviorOptimizations", + BindingFlags.NonPublic | BindingFlags.Static)!; + + SqlMapper.Settings.SetDefaults(); + var originalSingle = SqlMapper.Settings.UseSingleResultOptimization; + var originalRow = SqlMapper.Settings.UseSingleRowOptimization; + + try + { + // Exception message NOT mentioning SingleResult/SingleRow → returns false + var ex = new Exception("Some other error"); + var result = (bool)method.Invoke(null, new object[] + { + System.Data.CommandBehavior.SingleResult, + ex + })!; + + Assert.False(result); + } + finally + { + SqlMapper.Settings.UseSingleResultOptimization = originalSingle; + SqlMapper.Settings.UseSingleRowOptimization = originalRow; + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DisposedReader2.cs b/tests/Dapper.Tests/FakeDbTests.DisposedReader2.cs new file mode 100644 index 000000000..050a2d47e --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DisposedReader2.cs @@ -0,0 +1,103 @@ +#if !NET481 +using System; +using System.Collections; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DisposedReader coverage for methods not yet tested. + /// + public class FakeDbDisposedReader2Tests + { + [Fact] + public void DisposedReader_Depth_ReturnsZero() + => Assert.Equal(0, DisposedReader.Instance.Depth); + + [Fact] + public void DisposedReader_FieldCount_ReturnsZero() + => Assert.Equal(0, DisposedReader.Instance.FieldCount); + + [Fact] + public void DisposedReader_IsClosed_ReturnsTrue() + => Assert.True(DisposedReader.Instance.IsClosed); + + [Fact] + public void DisposedReader_HasRows_ReturnsFalse() + => Assert.False(DisposedReader.Instance.HasRows); + + [Fact] + public void DisposedReader_RecordsAffected_ReturnsMinusOne() + => Assert.Equal(-1, DisposedReader.Instance.RecordsAffected); + + [Fact] + public void DisposedReader_VisibleFieldCount_ReturnsZero() + => Assert.Equal(0, DisposedReader.Instance.VisibleFieldCount); + + [Fact] + public void DisposedReader_Close_DoesNotThrow() + { + // Close is a no-op — should not throw + DisposedReader.Instance.Close(); + } + + [Fact] + public void DisposedReader_GetSchemaTable_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetSchemaTable()); + + [Fact] + public void DisposedReader_GetEnumerator_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetEnumerator()); + + [Fact] + public void DisposedReader_GetFieldValue_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetFieldValue(0)); + + [Fact] + public void DisposedReader_GetProviderSpecificFieldType_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetProviderSpecificFieldType(0)); + + [Fact] + public void DisposedReader_GetProviderSpecificValue_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetProviderSpecificValue(0)); + + [Fact] + public void DisposedReader_GetProviderSpecificValues_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetProviderSpecificValues(Array.Empty())); + + [Fact] + public void DisposedReader_GetChar_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetChar(0)); + + [Fact] + public void DisposedReader_GetBytes_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetBytes(0, 0, null, 0, 0)); + + [Fact] + public void DisposedReader_GetChars_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetChars(0, 0, null, 0, 0)); + + [Fact] + public void DisposedReader_IndexerByInt_Throws() + => Assert.Throws(() => DisposedReader.Instance[0]); + + [Fact] + public void DisposedReader_IndexerByString_Throws() + => Assert.Throws(() => DisposedReader.Instance["x"]); + + [Fact] + public async Task DisposedReader_NextResultAsync_ThrowsObjectDisposedException() + { + await Assert.ThrowsAsync(() => + DisposedReader.Instance.NextResultAsync(CancellationToken.None)); + } + + [Fact] + public void DisposedReader_NextResult_Throws() + => Assert.Throws(() => DisposedReader.Instance.NextResult()); + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DynamicOutputAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.DynamicOutputAdvanced.cs new file mode 100644 index 000000000..a1018892e --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DynamicOutputAdvanced.cs @@ -0,0 +1,193 @@ +#if !NET481 +using System; +using System.Data; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DynamicParameters.Output<T> coverage: + /// - nested property chain (chain.Count > 1) → for-loop at lines 417-430 + /// - field as last member (Stfld, line 447) + /// - field as intermediate chain member (Ldfld, line 428) + /// - "param does not already exist" else path (lines 475-480) + /// - cached setter reuse (second call with same property) + /// - ThrowInvalidChain (line 365) + /// + public class FakeDbDynamicOutputAdvancedTests + { + private class Target + { + public int Id { get; set; } + public string? Name { get; set; } + } + + private class TargetWithField + { + public int Counter; + } + + private class Outer + { + public Inner Inner { get; set; } = new Inner(); + } + + private class Inner + { + public int Value { get; set; } + } + + private class OuterWithFieldChain + { + public Inner2 Item = new Inner2(); + } + + private class Inner2 + { + public int Val { get; set; } + } + + // ── "else" path: param does NOT already exist (lines 475-480) ── + // This hits when Output is called without a prior dp.Add for that param. + + [Fact] + public void Output_ParamNotPreAdded_CreatesNewParam() + { + var target = new Target { Id = 42 }; + var dp = new DynamicParameters(); + dp.Output(target, x => x.Id); // no prior dp.Add("Id", ...) + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @Id", dp); + + // OnCompleted fires, callback reads the attached param value and sets target.Id + Assert.Equal(42, target.Id); + } + + // ── String output without size — hits DbString.DefaultLength path ── + + [Fact] + public void Output_StringNoSize_UsesDefaultLength() + { + var target = new Target { Name = "Alice" }; + var dp = new DynamicParameters(); + dp.Output(target, x => x.Name); // no explicit size for string + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @Name", dp); + + Assert.Equal("Alice", target.Name); + } + + // ── Public field as last member (Stfld IL path, line 447) ────── + + [Fact] + public void Output_PublicField_CallbackFires() + { + var target = new TargetWithField { Counter = 99 }; + var dp = new DynamicParameters(); + dp.Output(target, x => (object?)x.Counter); // field, not property + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @Counter", dp); + + Assert.Equal(99, target.Counter); + } + + // ── Nested property chain (chain.Count > 1 → for-loop lines 417-430) ── + + [Fact] + public void Output_NestedPropertyChain_ForLoopRuns() + { + var target = new Outer { Inner = new Inner { Value = 10 } }; + var dp = new DynamicParameters(); + dp.Output(target, x => (object?)x.Inner.Value); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @InnerValue", dp); + + Assert.Equal(10, target.Inner.Value); + } + + // ── Nested field in chain (Ldfld IL path, line 428) ──────────── + + [Fact] + public void Output_FieldIntermediateChain_LdfldPath() + { + var target = new OuterWithFieldChain { Item = new Inner2 { Val = 55 } }; + var dp = new DynamicParameters(); + dp.Output(target, x => (object?)x.Item.Val); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @ItemVal", dp); + + Assert.Equal(55, target.Item.Val); + } + + // ── Cached setter reuse (second call with same property) ──────── + + [Fact] + public void Output_SameProperty_Twice_UsesCachedSetter() + { + var t1 = new Target { Id = 1 }; + var t2 = new Target { Id = 2 }; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + var dp1 = new DynamicParameters(); + dp1.Output(t1, x => x.Id); + conn.EnqueueNonQueryResult(1); + conn.Execute("EXEC SP @Id", dp1); + Assert.Equal(1, t1.Id); + + // second call — same property type should hit the cache (line 407) + var dp2 = new DynamicParameters(); + dp2.Output(t2, x => x.Id); + conn.EnqueueNonQueryResult(1); + conn.Execute("EXEC SP @Id", dp2); + Assert.Equal(2, t2.Id); + } + + // ── ThrowInvalidChain — constant expression body ───────────── + + [Fact] + public void Output_InvalidExpression_ThrowsInvalidOperation() + { + var target = new Target(); + var dp = new DynamicParameters(); + Assert.Throws(() => + dp.Output(target, x => (object?)42)); + } + + // ── Existing param path (TryGetValue = true, lines 466-473) ──── + + [Fact] + public void Output_ExistingParam_SetsDirectionToInputOutput() + { + var target = new Target { Id = 7 }; + var dp = new DynamicParameters(); + dp.Add("Id", 7, DbType.Int32, ParameterDirection.Input); + dp.Output(target, x => x.Id); // modifies existing param to InputOutput + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + conn.Execute("EXEC SP @Id", dp); + + Assert.Equal(7, target.Id); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DynamicParameters2.cs b/tests/Dapper.Tests/FakeDbTests.DynamicParameters2.cs new file mode 100644 index 000000000..722d797f9 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DynamicParameters2.cs @@ -0,0 +1,206 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbDynamicParameterTests + { + // ── ParameterNames ──────────────────────────────────────────── + + [Fact] + public void DynamicParameters_ParameterNames_ReflectsAdded() + { + var dp = new DynamicParameters(); + dp.Add("Foo", 1); + dp.Add("Bar", 2); + + var names = dp.ParameterNames.ToList(); + Assert.Contains("Foo", names); + Assert.Contains("Bar", names); + } + + [Fact] + public void DynamicParameters_ParameterNames_Empty_WhenNoParams() + { + var dp = new DynamicParameters(); + Assert.Empty(dp.ParameterNames); + } + + // ── Add overloads ───────────────────────────────────────────── + + [Fact] + public void DynamicParameters_Add_WithDbType() + { + var dp = new DynamicParameters(); + dp.Add("@p", "hello", DbType.String); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", "hello" } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT @p AS Val", dp); + Assert.Equal("hello", result); + } + + [Fact] + public void DynamicParameters_Add_WithSize() + { + var dp = new DynamicParameters(); + dp.Add("name", "Alice", DbType.String, ParameterDirection.Input, 50); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("UPDATE T SET Name = @name", dp); + } + + [Fact] + public void DynamicParameters_Add_WithPrecisionAndScale() + { + var dp = new DynamicParameters(); + dp.Add("amount", 99.99m, DbType.Decimal, ParameterDirection.Input, null, 10, 2); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("INSERT INTO Orders (Amount) VALUES (@amount)", dp); + } + + // ── Get ──────────────────────────────────────────────────── + + [Fact] + public void DynamicParameters_Get_ReturnsValue_AfterExecute() + { + var dp = new DynamicParameters(); + dp.Add("val", 42); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @val", dp); + + Assert.Equal(42, dp.Get("val")); + } + + [Fact] + public void DynamicParameters_Get_WithAtPrefix_StillWorks() + { + var dp = new DynamicParameters(); + dp.Add("@id", 7); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @id", dp); + + Assert.Equal(7, dp.Get("@id")); // @ should be stripped internally + } + + // ── Template constructor ────────────────────────────────────── + + [Fact] + public void DynamicParameters_FromObject_Template() + { + var template = new { id = 10, name = "Alice" }; + var dp = new DynamicParameters(template); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 10 }, { "Name", "Alice" } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users WHERE Id = @id AND Name = @name", dp) + .ToList(); + Assert.Single(result); + } + + [Fact] + public void DynamicParameters_FromDictionary_Template() + { + var dict = new Dictionary { { "id", 5 } }; + var dp = new DynamicParameters(dict); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 5 }, { "Name", "E" } } }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users WHERE Id = @id", dp).ToList(); + Assert.Single(result); + } + + [Fact] + public void DynamicParameters_MergedWithAnother() + { + var dp1 = new DynamicParameters(); + dp1.Add("a", 1); + + var dp2 = new DynamicParameters(dp1); + dp2.Add("b", 2); + + var names = dp2.ParameterNames.ToList(); + Assert.Contains("a", names); + Assert.Contains("b", names); + } + + // ── RemoveUnused ───────────────────────────────────────────── + + [Fact] + public void DynamicParameters_RemoveUnused_Default_IsTrue() + { + var dp = new DynamicParameters(); + Assert.True(dp.RemoveUnused); + } + + [Fact] + public void DynamicParameters_Execute_WithUnusedParam_RemoveUnused_True() + { + // RemoveUnused=true: params not referenced in SQL are removed + var dp = new DynamicParameters(); + dp.Add("used", 1); + dp.Add("unused", 99); + dp.RemoveUnused = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // Should not throw even with the unused param + conn.Execute("SELECT @used", dp); + } + + // ── IParameterLookup indexer ────────────────────────────────── + + [Fact] + public void DynamicParameters_Indexer_ReturnsValue() + { + var dp = new DynamicParameters(); + dp.Add("key", "value"); + + SqlMapper.IParameterLookup lookup = dp; + Assert.Equal("value", lookup["key"]); + } + + [Fact] + public void DynamicParameters_Indexer_ReturnsNull_ForMissingKey() + { + var dp = new DynamicParameters(); + SqlMapper.IParameterLookup lookup = dp; + Assert.Null(lookup["missing"]); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DynamicParamsAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.DynamicParamsAdvanced.cs new file mode 100644 index 000000000..56877c8bb --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DynamicParamsAdvanced.cs @@ -0,0 +1,188 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional DynamicParameters coverage: sub-DynamicParameters with templates, + /// PackListParameters (EnumerableMultiParameter), Get<T> with DBNull, + /// parameter reuse, TypeHandler path, and Output callback basics. + /// + public class FakeDbDynamicParamsAdvancedTests + { + // ── Sub-DynamicParameters with templates ────────────────────── + // Lines 63-69: when the sub-DP itself has templates + + [Fact] + public void DynamicParameters_AddFromDynamicParameters_WithTemplate_Works() + { + // Create a DynamicParameters that uses an anonymous object template + var inner = new DynamicParameters(new { Name = "Alice" }); + + // Add it to another DynamicParameters (triggers lines 63-69 path) + var outer = new DynamicParameters(); + outer.AddDynamicParams(inner); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + // If inner had templates, they should be copied to outer's templates + conn.Execute("UPDATE T SET Name = @Name", outer); + } + + // ── IEnumerable> template ───────── + + [Fact] + public void DynamicParameters_AddFromDictionary_Works() + { + var dict = new Dictionary { { "Id", 1 }, { "Name", "Bob" } }; + var dp = new DynamicParameters(); + dp.AddDynamicParams(dict); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("UPDATE T SET Name = @Name WHERE Id = @Id", dp); + } + + // ── PackListParameters (EnumerableMultiParameter) ───────────── + // Lines 246-250: adding a list parameter triggers PackListParameters + + [Fact] + public void DynamicParameters_ListParam_ExpandsInClause() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } }, + new Dictionary { { "Id", 2 } }, + }); + conn.Open(); + + var ids = new[] { 1, 2, 3 }; + // When a list is passed as param it becomes EnumerableMultiParameter + var results = conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + Assert.Equal(2, results.Count); + } + + // ── Get with DBNull — non-nullable type throws ───────────── + // Lines 324-325 + + [Fact] + public void DynamicParameters_Get_DBNull_NonNullable_Throws() + { + var dp = new DynamicParameters(); + dp.Add("val", DBNull.Value, DbType.Int32, ParameterDirection.Input); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @val", dp); + + // Get where the attached param's value is DBNull throws ApplicationException + Assert.Throws(() => dp.Get("val")); + } + + // ── Get with DBNull — nullable type returns default ───────── + + [Fact] + public void DynamicParameters_Get_DBNull_Nullable_ReturnsDefault() + { + var dp = new DynamicParameters(); + dp.Add("val", DBNull.Value, DbType.Int32, ParameterDirection.Input); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @val", dp); + + var result = dp.Get("val"); + Assert.Null(result); + } + + // ── TypeHandler path in AddParameters ───────────────────────── + // Lines 285-290: when a custom TypeHandler is registered + + private class MyGuid + { + public Guid Value { get; } + public MyGuid(Guid value) { Value = value; } + } + + private class MyGuidHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, MyGuid value) + { + parameter.DbType = DbType.Guid; + parameter.Value = value.Value; + } + public override MyGuid Parse(object value) => new MyGuid((Guid)value); + } + + [Fact] + public void DynamicParameters_TypeHandler_SetValue_Path() + { + SqlMapper.AddTypeHandler(new MyGuidHandler()); + try + { + var guid = new MyGuid(Guid.NewGuid()); + var dp = new DynamicParameters(); + dp.Add("g", guid, DbType.Guid); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @g", dp); + } + finally + { + SqlMapper.RemoveTypeMap(typeof(MyGuid)); + } + } + + // ── Parameter already in command (reuse path) ───────────────── + // Lines 261-263: when template adds param to command, then explicit param + // with same name is processed — command.Parameters.Contains returns true + + [Fact] + public void DynamicParameters_TemplateAndExplicit_SameName_ReusesParam() + { + // Template adds "Name" to command, then explicit "Name" param reuses it + var dp = new DynamicParameters(new { Name = "FromTemplate" }); + dp.Add("Name", "Explicit"); // explicit param with same name as template property + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("UPDATE T SET Name = @Name", dp); + } + + // ── ShouldSetDbType non-nullable overload ───────────────────── + // Line 162 + + [Fact] + public void DynamicParameters_WithExplicitDbType_Works() + { + var dp = new DynamicParameters(); + dp.Add("n", 42, DbType.Int32); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("SELECT @n", dp); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DynamicRow.cs b/tests/Dapper.Tests/FakeDbTests.DynamicRow.cs new file mode 100644 index 000000000..01356b45a --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DynamicRow.cs @@ -0,0 +1,309 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for DapperRow (the object returned by Query<dynamic>) covering + /// its IDictionary interface, iteration, and metadata. + /// + public class FakeDbDynamicRowTests + { + // ── IDictionary interface ───────────────────── + + [Fact] + public void DynamicRow_CastToIDictionary_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var dict = (IDictionary)row; + + Assert.Equal(1, dict["Id"]); + Assert.Equal("Alice", dict["Name"]); + } + + [Fact] + public void DynamicRow_IDictionary_ContainsKey() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Bob" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var dict = (IDictionary)row; + + Assert.True(dict.ContainsKey("Id")); + Assert.True(dict.ContainsKey("Name")); + Assert.False(dict.ContainsKey("Missing")); + } + + [Fact] + public void DynamicRow_IDictionary_Keys() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 }, { "C", 3 } } + }); + conn.Open(); + + var row = conn.Query("SELECT A, B, C FROM T").Single(); + var keys = ((IDictionary)row).Keys.ToList(); + + Assert.Equal(3, keys.Count); + Assert.Contains("A", keys); + Assert.Contains("B", keys); + Assert.Contains("C", keys); + } + + [Fact] + public void DynamicRow_IDictionary_Values() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "X", 10 }, { "Y", 20 } } + }); + conn.Open(); + + var row = conn.Query("SELECT X, Y FROM T").Single(); + var values = ((IDictionary)row).Values.ToList(); + + Assert.Equal(2, values.Count); + Assert.Contains(10, values); + Assert.Contains(20, values); + } + + [Fact] + public void DynamicRow_IDictionary_Count() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 }, { "C", 3 } } + }); + conn.Open(); + + var row = conn.Query("SELECT A, B, C FROM T").Single(); + Assert.Equal(3, ((IDictionary)row).Count); + } + + [Fact] + public void DynamicRow_IDictionary_TryGetValue_Found() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 42 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + var dict = (IDictionary)row; + var found = dict.TryGetValue("Id", out var val); + + Assert.True(found); + Assert.Equal(42, val); + } + + [Fact] + public void DynamicRow_IDictionary_TryGetValue_NotFound() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + var dict = (IDictionary)row; + var found = dict.TryGetValue("Missing", out var val); + + Assert.False(found); + Assert.Null(val); + } + + // ── Enumeration ─────────────────────────────────────────────── + + [Fact] + public void DynamicRow_Enumeration_YieldsKeyValuePairs() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var pairs = ((IEnumerable>)row).ToList(); + + Assert.Equal(2, pairs.Count); + Assert.Contains(pairs, p => p.Key == "Id" && (int)p.Value == 1); + Assert.Contains(pairs, p => p.Key == "Name" && (string)p.Value == "Alice"); + } + + [Fact] + public void DynamicRow_MultipleRows_EachEnumerable() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + var rows = conn.Query("SELECT Id, Name FROM T").ToList(); + foreach (var row in rows) + { + var dict = (IDictionary)row; + Assert.True(dict.ContainsKey("Id")); + Assert.True(dict.ContainsKey("Name")); + } + } + + // ── Dynamic member access (exercises DapperRowMetaObject) ───── + + [Fact] + public void DynamicRow_DynamicAccess_MemberNotFound_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + dynamic row = conn.Query("SELECT Id FROM T").Single(); + + // Accessing a member that doesn't exist in the result set should return null + Assert.Null(row.DoesNotExist); + } + + [Fact] + public void DynamicRow_SetValue_ViaIDictionary() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var dict = (IDictionary)row; + + // Updating an existing key + dict["Name"] = "Updated"; + Assert.Equal("Updated", dict["Name"]); + } + + [Fact] + public void DynamicRow_IDictionary_IsReadOnly_IsFalse() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + Assert.False(((IDictionary)row).IsReadOnly); + } + + [Fact] + public void DynamicRow_IDictionary_Remove_ThenNotContains() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var dict = (IDictionary)row; + + dict.Remove("Name"); + Assert.False(dict.ContainsKey("Name")); + } + + [Fact] + public void DynamicRow_IDictionary_Add_NewKey() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + var dict = (IDictionary)row; + + dict["NewKey"] = "NewValue"; + Assert.True(dict.ContainsKey("NewKey")); + Assert.Equal("NewValue", dict["NewKey"]); + } + + [Fact] + public void DynamicRow_IDictionary_CopyTo_Array() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var coll = (ICollection>)row; + var array = new KeyValuePair[2]; + coll.CopyTo(array, 0); + + Assert.Equal(2, array.Length); + } + + // ── DapperTable sharing ─────────────────────────────────────── + + [Fact] + public void DynamicRow_MultipleRows_ShareSameTableSchema() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + new Dictionary { { "Id", 3 }, { "Name", "C" } }, + }); + conn.Open(); + + var rows = conn.Query("SELECT Id, Name FROM T").ToList(); + Assert.Equal(3, rows.Count); + + // All rows should expose same columns + foreach (dynamic row in rows) + { + var dict = (IDictionary)row; + Assert.True(dict.ContainsKey("Id")); + Assert.True(dict.ContainsKey("Name")); + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.DynamicRowAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.DynamicRowAdvanced.cs new file mode 100644 index 000000000..fa43c68cf --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.DynamicRowAdvanced.cs @@ -0,0 +1,161 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Advanced DapperRow/DapperRowMetaObject tests: dynamic set, delete via IDictionary, + /// TypeDescriptor integration, and DapperTable sharing. + /// + public class FakeDbDynamicRowAdvancedTests + { + // ── Dynamic SET member (BindSetMember on DapperRowMetaObject) ── + + [Fact] + public void DynamicRow_DynamicSet_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + dynamic row = conn.Query("SELECT Id, Name FROM T").Single(); + + // Trigger BindSetMember on DapperRowMetaObject + row.Name = "Updated"; + Assert.Equal("Updated", (string)row.Name); + } + + [Fact] + public void DynamicRow_DynamicSet_NewMember_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + dynamic row = conn.Query("SELECT Id FROM T").Single(); + row.NewField = "added"; + Assert.Equal("added", (string)row.NewField); + } + + // ── DapperTable operations ───────────────────────────────────── + + [Fact] + public void DapperRow_IDictionary_Clear_RemovesAllEntries() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 }, { "C", 3 } } + }); + conn.Open(); + + var row = conn.Query("SELECT A, B, C FROM T").Single(); + var dict = (IDictionary)row; + + dict.Clear(); + Assert.Empty(dict); + } + + [Fact] + public void DapperRow_IDictionary_Contains_KeyValuePair() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 42 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + var coll = (ICollection>)row; + + Assert.True(coll.Contains(new KeyValuePair("Id", 42))); + Assert.False(coll.Contains(new KeyValuePair("Id", 99))); + } + + [Fact] + public void DapperRow_IDictionary_Remove_KeyValuePair_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id, Name FROM T").Single(); + var coll = (ICollection>)row; + + bool removed = coll.Remove(new KeyValuePair("Name", "Alice")); + Assert.True(removed); + Assert.False(((IDictionary)row).ContainsKey("Name")); + } + + // ── DapperRow.ToString() ─────────────────────────────────────── + + [Fact] + public void DapperRow_ToString_DoesNotThrow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var row = (IDictionary)conn.Query("SELECT Id FROM T").Single(); + // Calling ToString via IDictionary cast (not dynamic dispatch) + var str = row.ToString(); + // DapperRow.ToString() is not overridden, so returns type name + } + + // ── DapperRow as IEnumerable (non-generic) ──────────────────── + + [Fact] + public void DapperRow_NonGenericEnumerable_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 } } + }); + conn.Open(); + + var row = conn.Query("SELECT A, B FROM T").Single(); + var count = 0; + foreach (KeyValuePair pair in (System.Collections.IEnumerable)row) + { + count++; + } + Assert.Equal(2, count); + } + + // ── DapperRow equality/comparisons ──────────────────────────── + + [Fact] + public void DapperRow_GetHashCode_DoesNotThrow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + var row = conn.Query("SELECT Id FROM T").Single(); + var _ = row.GetHashCode(); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Errors.cs b/tests/Dapper.Tests/FakeDbTests.Errors.cs new file mode 100644 index 000000000..4f50c793c --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Errors.cs @@ -0,0 +1,92 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using pengdows.crud.enums; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbErrorTests + { + [Fact] + public void QueryFirst_ThrowsInvalidOperationException_OnEmptyResult() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + Assert.Throws(() => + conn.QueryFirst("SELECT Id, Name FROM Users WHERE 1=0")); + } + + [Fact] + public void QuerySingle_ThrowsInvalidOperationException_OnMultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + Assert.Throws(() => + conn.QuerySingle("SELECT Id, Name FROM Users")); + } + + [Fact] + public void ConnectionFailOnOpen_ThrowsWhenOpen() + { + var factory = new fakeDbFactory(SupportedDatabase.Sqlite); + var conn = (fakeDbConnection)factory.CreateConnection(); + conn.SetFailOnOpen(true, false); + + Assert.ThrowsAny(() => conn.Open()); + } + + [Fact] + public void ConnectionFailOnOpen_WithCustomException_ThrowsThatException() + { + var factory = fakeDbFactory.CreateFailingFactory( + SupportedDatabase.Sqlite, + ConnectionFailureMode.FailOnOpen, + new TimeoutException("db timeout"), + null); + var conn = factory.CreateConnection(); + + Assert.Throws(() => conn.Open()); + } + + [Fact] + public void ConnectionFailAfterCount_FailsAfterNthOpen() + { + var factory = fakeDbFactory.CreateFailingFactory( + SupportedDatabase.Sqlite, + ConnectionFailureMode.FailAfterCount, + new InvalidOperationException("too many opens"), + 2); + var conn = (fakeDbConnection)factory.CreateConnection(); + + conn.Open(); conn.Close(); // 1st – ok + conn.Open(); conn.Close(); // 2nd – ok + + Assert.ThrowsAny(() => conn.Open()); // 3rd – fails + } + + [Fact] + public void ResetFailureConditions_AllowsConnectionToWork() + { + var factory = new fakeDbFactory(SupportedDatabase.Sqlite); + var conn = (fakeDbConnection)factory.CreateConnection(); + conn.SetFailOnOpen(true, false); + conn.ResetFailureConditions(); + + conn.Open(); + Assert.Equal(ConnectionState.Open, conn.State); + conn.Close(); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Execute.cs b/tests/Dapper.Tests/FakeDbTests.Execute.cs new file mode 100644 index 000000000..dbc75d476 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Execute.cs @@ -0,0 +1,89 @@ +#if !NET481 +using System.Collections.Generic; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbExecuteTests + { + [Fact] + public void Execute_ReturnsAffectedRowCount() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(3); + conn.Open(); + + var rows = conn.Execute("DELETE FROM Users WHERE Active = 0"); + + Assert.Equal(3, rows); + } + + [Fact] + public void Execute_ReturnsZero_WhenNoRowsAffected() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + var rows = conn.Execute("UPDATE Users SET Name = 'x' WHERE Id = -1"); + + Assert.Equal(0, rows); + } + + [Fact] + public void ExecuteScalar_ReturnsPreloadedValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(42L); + conn.Open(); + + var count = conn.ExecuteScalar("SELECT COUNT(*) FROM Users"); + + Assert.Equal(42L, count); + } + + [Fact] + public void ExecuteScalar_ReturnsString() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult("hello"); + conn.Open(); + + var result = conn.ExecuteScalar("SELECT Name FROM Users WHERE Id = 1"); + + Assert.Equal("hello", result); + } + + [Fact] + public void ExecuteScalar_ReturnsDefault_WhenNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(null); + conn.Open(); + + var result = conn.ExecuteScalar("SELECT Name FROM Users WHERE Id = 99"); + + Assert.Null(result); + } + + [Fact] + public void ExecuteReader_ReturnsReadableResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + + int count = 0; + while (reader.Read()) count++; + Assert.Equal(2, count); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.ExtensionsCastResult.cs b/tests/Dapper.Tests/FakeDbTests.ExtensionsCastResult.cs new file mode 100644 index 000000000..00c274138 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.ExtensionsCastResult.cs @@ -0,0 +1,90 @@ +#if !NET481 +using System; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers Extensions.CastResult<TFrom,TTo> (lines 12-22) and OnTaskCompleted (lines 25-42) + /// via reflection. Triggers the async continuation path when the source task is not yet complete. + /// + public class FakeDbExtensionsCastResultTests + { + private static MethodInfo GetCastResult() + { + var type = typeof(SqlMapper).Assembly.GetType("Dapper.Extensions")!; + return type.GetMethod("CastResult", + BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public)!; + } + + // ── Fast path: already-completed task (RanToCompletion) ─────── + + [Fact] + public async Task CastResult_CompletedTask_FastPath() + { + var method = GetCastResult().MakeGenericMethod(typeof(string), typeof(object)); + var completed = Task.FromResult("hello"); + + var result = (Task)method.Invoke(null, new object[] { completed })!; + Assert.Equal("hello", await result); + } + + // ── Null task → ArgumentNullException ───────────────────────── + + [Fact] + public void CastResult_NullTask_Throws() + { + var method = GetCastResult().MakeGenericMethod(typeof(string), typeof(object)); + var ex = Assert.Throws(() => + method.Invoke(null, new object?[] { null })); + Assert.IsType(ex.InnerException); + } + + // ── Async path: RanToCompletion (OnTaskCompleted SetResult) ─── + + [Fact] + public async Task CastResult_AsyncPath_RanToCompletion() + { + var method = GetCastResult().MakeGenericMethod(typeof(string), typeof(object)); + var tcs = new TaskCompletionSource(); + + var castTask = (Task)method.Invoke(null, new object[] { tcs.Task })!; + Assert.False(castTask.IsCompleted); // not yet done + + tcs.SetResult("world"); + var result = await castTask; + Assert.Equal("world", result); + } + + // ── Async path: Canceled (OnTaskCompleted SetCanceled) ──────── + + [Fact] + public async Task CastResult_AsyncPath_Canceled() + { + var method = GetCastResult().MakeGenericMethod(typeof(string), typeof(object)); + var tcs = new TaskCompletionSource(); + + var castTask = (Task)method.Invoke(null, new object[] { tcs.Task })!; + tcs.SetCanceled(); + + await Assert.ThrowsAnyAsync(() => castTask); + } + + // ── Async path: Faulted (OnTaskCompleted SetException) ──────── + + [Fact] + public async Task CastResult_AsyncPath_Faulted() + { + var method = GetCastResult().MakeGenericMethod(typeof(string), typeof(object)); + var tcs = new TaskCompletionSource(); + + var castTask = (Task)method.Invoke(null, new object[] { tcs.Task })!; + tcs.SetException(new InvalidOperationException("async error")); + + await Assert.ThrowsAsync(() => castTask); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReader.cs b/tests/Dapper.Tests/FakeDbTests.GridReader.cs new file mode 100644 index 000000000..8819d1303 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReader.cs @@ -0,0 +1,279 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbGridReaderTests + { + // Enqueue two result sets: first for Users, second for Products + private static fakeDbConnection TwoResultSetConnection() + { + var conn = new fakeDbConnection(new FakeDataStore()); + // First result set + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + // Second result set (used by NextResult) + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "ProductId", 10 }, { "ProductName", "Widget" } }, + }); + conn.Open(); + return conn; + } + + [Fact] + public void QueryMultiple_ReadFirst_ReturnsUsers() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ...; SELECT ..."); + var users = grid.Read().ToList(); + + Assert.Equal(2, users.Count); + Assert.Equal("Alice", users[0].Name); + Assert.Equal("Bob", users[1].Name); + } + + [Fact] + public void QueryMultiple_ReadDynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Eve" } } + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var rows = grid.Read().ToList(); + + Assert.Single(rows); + Assert.Equal(5, (int)rows[0].Id); + } + + [Fact] + public void QueryMultiple_ReadFirst_ReturnsFirst() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var first = grid.ReadFirst(); + + Assert.Equal(1, first.Id); + } + + [Fact] + public void QueryMultiple_ReadFirstOrDefault_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var result = grid.ReadFirstOrDefault(); + + Assert.Null(result); + } + + [Fact] + public void QueryMultiple_ReadSingle_ThrowsOnMultiple() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + Assert.Throws(() => grid.ReadSingle()); + } + + [Fact] + public void QueryMultiple_ReadSingle_ReturnsRow_WhenExactlyOne() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Solo" } } + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var row = grid.ReadSingle(); + + Assert.Equal(7, row.Id); + } + + [Fact] + public void QueryMultiple_ReadSingleOrDefault_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var result = grid.ReadSingleOrDefault(); + + Assert.Null(result); + } + + [Fact] + public void QueryMultiple_ReadFirst_ThrowsOnEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + Assert.Throws(() => grid.ReadFirst()); + } + + [Fact] + public void QueryMultiple_ReadUnbuffered_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + using var grid = conn.QueryMultiple("SELECT ..."); + var result = grid.Read(buffered: false).ToList(); + + Assert.Equal(2, result.Count); + } + + // ── async variants ──────────────────────────────────────────── + + [Fact] + public async Task QueryMultiple_ReadAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var users = (await grid.ReadAsync()).ToList(); + + Assert.Equal(2, users.Count); + } + + [Fact] + public async Task QueryMultiple_ReadFirstAsync_ReturnsFirst() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Three" } }, + new Dictionary { { "Id", 4 }, { "Name", "Four" } }, + }); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var first = await grid.ReadFirstAsync(); + + Assert.Equal(3, first.Id); + } + + [Fact] + public async Task QueryMultiple_ReadFirstOrDefaultAsync_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var result = await grid.ReadFirstOrDefaultAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task QueryMultiple_ReadSingleAsync_ReturnsRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 9 }, { "Name", "Nine" } } + }); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var result = await grid.ReadSingleAsync(); + + Assert.Equal(9, result.Id); + } + + [Fact] + public async Task QueryMultiple_ReadSingleOrDefaultAsync_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var result = await grid.ReadSingleOrDefaultAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task QueryMultiple_ReadDynamicAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + using var grid = await conn.QueryMultipleAsync("SELECT ..."); + var rows = (await grid.ReadAsync()).ToList(); + + Assert.Single(rows); + } + + [Fact] + public void QueryMultiple_Dispose_DoesNotThrow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } } + }); + conn.Open(); + + var grid = conn.QueryMultiple("SELECT ..."); + grid.Dispose(); // Should not throw even without reading + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReaderAsync2.cs b/tests/Dapper.Tests/FakeDbTests.GridReaderAsync2.cs new file mode 100644 index 000000000..b33afbae2 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReaderAsync2.cs @@ -0,0 +1,234 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional GridReader async tests covering dynamic overloads, Type-based overloads, + /// and unbuffered async paths. + /// + public class FakeDbGridReaderAsync2Tests + { + private class Item { public int Id { get; set; } public string? Name { get; set; } } + + // ── Dynamic async read methods ──────────────────────────────── + + [Fact] + public async Task GridReader_ReadFirstAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + dynamic row = await multi.ReadFirstAsync(); + + Assert.Equal(1, (int)row.Id); + } + + [Fact] + public async Task GridReader_ReadFirstOrDefaultAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 2 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + dynamic? row = await multi.ReadFirstOrDefaultAsync(); + + Assert.NotNull(row); + Assert.Equal(2, (int)row!.Id); + } + + [Fact] + public async Task GridReader_ReadFirstOrDefaultAsync_Dynamic_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var row = await multi.ReadFirstOrDefaultAsync(); + + Assert.Null(row); + } + + [Fact] + public async Task GridReader_ReadSingleAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + dynamic row = await multi.ReadSingleAsync(); + + Assert.Equal(3, (int)row.Id); + } + + [Fact] + public async Task GridReader_ReadSingleOrDefaultAsync_Dynamic_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var row = await multi.ReadSingleOrDefaultAsync(); + + Assert.Null(row); + } + + // ── Type-based async read methods ───────────────────────────── + + [Fact] + public async Task GridReader_ReadAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var results = (await multi.ReadAsync(typeof(Item))).ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal("Alice", ((Item)results[0]).Name); + } + + [Fact] + public async Task GridReader_ReadFirstAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Charlie" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = await multi.ReadFirstAsync(typeof(Item)); + + Assert.Equal(5, ((Item)row).Id); + } + + [Fact] + public async Task GridReader_ReadFirstOrDefaultAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 6 }, { "Name", "Dave" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = await multi.ReadFirstOrDefaultAsync(typeof(Item)); + + Assert.NotNull(row); + Assert.Equal(6, ((Item)row!).Id); + } + + [Fact] + public async Task GridReader_ReadFirstOrDefaultAsync_ByType_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = await multi.ReadFirstOrDefaultAsync(typeof(Item)); + + Assert.Null(row); + } + + [Fact] + public async Task GridReader_ReadSingleAsync_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Eve" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = await multi.ReadSingleAsync(typeof(Item)); + + Assert.Equal(7, ((Item)row).Id); + } + + [Fact] + public async Task GridReader_ReadSingleOrDefaultAsync_ByType_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = await multi.ReadSingleOrDefaultAsync(typeof(Item)); + + Assert.Null(row); + } + + // ── ReadAsync(buffered: false) — triggers unbuffered deferred path ── + + [Fact] + public async Task GridReader_ReadAsync_Unbuffered_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var results = (await multi.ReadAsync(buffered: false)).ToList(); + + Assert.Equal(2, results.Count); + } + + // ── ReadUnbufferedAsync() — dynamic variant ─────────────────── + + [Fact] + public async Task GridReader_ReadUnbufferedAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 99 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var results = new List(); + await foreach (var item in multi.ReadUnbufferedAsync()) + { + results.Add(item); + } + + Assert.Single(results); + Assert.Equal(99, (int)results[0].Id); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReaderMisc.cs b/tests/Dapper.Tests/FakeDbTests.GridReaderMisc.cs new file mode 100644 index 000000000..54d9a7b2b --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReaderMisc.cs @@ -0,0 +1,187 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers GridReader sync dynamic overloads (ReadFirstOrDefault/ReadSingleOrDefault), + /// async ThrowZeroRows path (ReadFirstAsync on empty), CommandDefinition with Transaction, + /// and QueryMultiple with multiple result sets (OnAfterGrid NextResult path). + /// + public class FakeDbGridReaderMiscTests + { + private class Item { public int Id { get; set; } public string? Name { get; set; } } + + // ── Sync ReadFirstOrDefault() — dynamic, no type arg ────────── + // Line 77 in SqlMapper.GridReader.cs + + [Fact] + public void GridReader_ReadFirstOrDefault_Dynamic_WithValue_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + dynamic? row = multi.ReadFirstOrDefault(); + + Assert.NotNull(row); + Assert.Equal(1, (int)row!.Id); + } + + [Fact] + public void GridReader_ReadFirstOrDefault_Dynamic_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var row = multi.ReadFirstOrDefault(); + + Assert.Null(row); + } + + // ── Sync ReadSingleOrDefault() — dynamic, no type arg ───────── + // Line 89 in SqlMapper.GridReader.cs + + [Fact] + public void GridReader_ReadSingleOrDefault_Dynamic_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var row = multi.ReadSingleOrDefault(); + + Assert.Null(row); + } + + [Fact] + public void GridReader_ReadSingleOrDefault_Dynamic_WithValue_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + dynamic? row = multi.ReadSingleOrDefault(); + Assert.NotNull(row); + Assert.Equal(7, (int)row!.Id); + } + + // ── ReadFirstAsync on empty throws (lines 221-223) ──────────── + + [Fact] + public async Task GridReader_ReadFirstAsync_EmptySet_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + await Assert.ThrowsAsync(() => multi.ReadFirstAsync()); + } + + [Fact] + public async Task GridReader_ReadSingleAsync_EmptySet_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + await Assert.ThrowsAsync(() => multi.ReadSingleAsync()); + } + + // ── Sync ReadFirst on empty throws ─────────────────────────── + + [Fact] + public void GridReader_ReadFirst_EmptySet_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadFirst()); + } + + [Fact] + public void GridReader_ReadSingle_EmptySet_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadSingle()); + } + + // ── CommandDefinition with Transaction (line 127) ───────────── + + [Fact] + public void CommandDefinition_WithTransaction_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + using var tx = conn.BeginTransaction(); + + conn.EnqueueNonQueryResult(1); + var cmd = new CommandDefinition("UPDATE T SET Val = 1", transaction: tx); + var rowsAffected = conn.Execute(cmd); + + Assert.Equal(1, rowsAffected); + } + + // ── CommandDefinition.CommandType via reflection (line 52) ──── + // The property is [Obsolete(error)] in DEBUG, so access via reflection + + [Fact] + public void CommandDefinition_CommandType_Accessible_ViaReflection() + { + var cmd = new CommandDefinition("SELECT 1", commandType: CommandType.Text); + var prop = typeof(CommandDefinition).GetProperty("CommandType"); + if (prop is not null) + { + var val = prop.GetValue(cmd); + Assert.Equal(CommandType.Text, val); + } + } + + // ── GridReader ConvertTo type conversion path (lines 489-490) ─ + + [Fact] + public void GridReader_Read_TypeConversion_Works() + { + // Returning a long from DB but reading as int triggers Convert.ChangeType + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1L } } // long value + }); + conn.Open(); + + // QueryFirst where DB returns long -> Convert.ChangeType path + using var multi = conn.QueryMultiple("SELECT 1"); + var results = multi.Read().ToList(); + Assert.Single(results); + Assert.Equal(1, results[0]); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReaderMultiMap.cs b/tests/Dapper.Tests/FakeDbTests.GridReaderMultiMap.cs new file mode 100644 index 000000000..e015ade65 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReaderMultiMap.cs @@ -0,0 +1,148 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for GridReader multi-type Read methods (multi-map within QueryMultiple result sets). + /// These cover MultiReadInternal and ReadDeferred paths. + /// + public class FakeDbGridReaderMultiMapTests + { + private class Owner { public int Id { get; set; } public string? Name { get; set; } } + private class Pet { public int PetId { get; set; } public string? Breed { get; set; } } + private class Tag { public int TagId { get; set; } public string? Label { get; set; } } + + // ── GridReader multi-type Read ───────────────────────────────── + + [Fact] + public void GridReader_Read_TwoTypes_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "PetId", 10 }, { "Breed", "Labrador" } + } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT ..."); + var result = multi.Read( + (o, p) => (o, p), splitOn: "PetId").ToList(); + + Assert.Single(result); + Assert.Equal("Alice", result[0].Item1.Name); + Assert.Equal("Labrador", result[0].Item2.Breed); + } + + [Fact] + public void GridReader_Read_ThreeTypes_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "PetId", 10 }, { "Breed", "Lab" }, + { "TagId", 100 }, { "Label", "Vaccinated" } + } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT ..."); + var result = multi.Read( + (o, p, t) => (o, p, t), splitOn: "PetId,TagId").ToList(); + + Assert.Single(result); + Assert.Equal("Alice", result[0].Item1.Name); + Assert.Equal("Lab", result[0].Item2.Breed); + Assert.Equal("Vaccinated", result[0].Item3.Label); + } + + [Fact] + public void GridReader_Read_TypeArrayOverload_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 5 }, { "Name", "Bob" }, + { "PetId", 50 }, { "Breed", "Pug" } + } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT ..."); + var result = multi.Read<(Owner, Pet)>( + new[] { typeof(Owner), typeof(Pet) }, + objs => ((Owner)objs[0], (Pet)objs[1]), + splitOn: "PetId").ToList(); + + Assert.Single(result); + Assert.Equal("Bob", result[0].Item1.Name); + } + + // ── GridReader unbuffered Read ───────────────────────────── + + [Fact] + public void GridReader_Read_Unbuffered_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Users"); + // buffered: false goes through ReadDeferred + var results = multi.Read(buffered: false).ToList(); + + Assert.Equal(2, results.Count); + } + + // ── GridReader async multi-type reads ───────────────────────── + + [Fact] + public async Task GridReader_ReadAsync_Generic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Carol" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = (await multi.ReadAsync()).ToList(); + + Assert.Single(results); + Assert.Equal("Carol", results[0].Name); + } + + [Fact] + public async Task GridReader_ReadAsync_WithType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 4 }, { "Name", "Dave" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = (await multi.ReadAsync(typeof(Owner))).ToList(); + + Assert.Single(results); + Assert.Equal("Dave", ((Owner)results[0]).Name); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReaderSyncType.cs b/tests/Dapper.Tests/FakeDbTests.GridReaderSyncType.cs new file mode 100644 index 000000000..68f492f80 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReaderSyncType.cs @@ -0,0 +1,317 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for GridReader sync Type-based overloads and 4/5/6/7-type Read multimap variants. + /// + public class FakeDbGridReaderSyncTypeTests + { + private class A { public int Id { get; set; } public string? Name { get; set; } } + private class B { public int BId { get; set; } public string? BName { get; set; } } + private class C { public int CId { get; set; } } + private class D { public int DId { get; set; } } + private class E { public int EId { get; set; } } + private class F { public int FId { get; set; } } + private class G { public int GId { get; set; } } + + private static fakeDbConnection MakeConn(IReadOnlyList> rows) + { + var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(rows); + conn.Open(); + return conn; + } + + // ── Sync Read(Type type) ────────────────────────────────────── + + [Fact] + public void GridReader_Read_ByType_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var results = multi.Read(typeof(A)).Cast().ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal("Alice", results[0].Name); + } + + [Fact] + public void GridReader_Read_ByType_Null_Throws() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.Read(null!).ToList()); + } + + [Fact] + public void GridReader_Read_ByType_Unbuffered_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Carol" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var results = multi.Read(typeof(A), buffered: false).Cast().ToList(); + + Assert.Single(results); + Assert.Equal(3, results[0].Id); + } + + // ── Sync ReadFirst(Type) ────────────────────────────────────── + + [Fact] + public void GridReader_ReadFirst_ByType_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Dave" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = (A)multi.ReadFirst(typeof(A)); + + Assert.Equal(5, row.Id); + } + + [Fact] + public void GridReader_ReadFirst_ByType_Null_Throws() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadFirst(null!)); + } + + // ── Sync ReadFirstOrDefault(Type) ───────────────────────────── + + [Fact] + public void GridReader_ReadFirstOrDefault_ByType_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 6 }, { "Name", "Eve" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = (A?)multi.ReadFirstOrDefault(typeof(A)); + + Assert.NotNull(row); + Assert.Equal(6, row!.Id); + } + + [Fact] + public void GridReader_ReadFirstOrDefault_ByType_EmptySet_ReturnsNull() + { + using var conn = MakeConn(Array.Empty>()); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = multi.ReadFirstOrDefault(typeof(A)); + + Assert.Null(row); + } + + [Fact] + public void GridReader_ReadFirstOrDefault_ByType_Null_Throws() + { + using var conn = MakeConn(Array.Empty>()); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadFirstOrDefault(null!)); + } + + // ── Sync ReadSingle(Type) ───────────────────────────────────── + + [Fact] + public void GridReader_ReadSingle_ByType_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Frank" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = (A)multi.ReadSingle(typeof(A)); + + Assert.Equal(7, row.Id); + } + + [Fact] + public void GridReader_ReadSingle_ByType_Null_Throws() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadSingle(null!)); + } + + // ── Sync ReadSingleOrDefault(Type) ──────────────────────────── + + [Fact] + public void GridReader_ReadSingleOrDefault_ByType_EmptySet_ReturnsNull() + { + using var conn = MakeConn(Array.Empty>()); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var row = multi.ReadSingleOrDefault(typeof(A)); + + Assert.Null(row); + } + + [Fact] + public void GridReader_ReadSingleOrDefault_ByType_Null_Throws() + { + using var conn = MakeConn(Array.Empty>()); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + Assert.Throws(() => multi.ReadSingleOrDefault(null!)); + } + + // ── 4-type Read multimap ────────────────────────────────────── + + [Fact] + public void GridReader_Read_4Types_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, + { "DId", 4 } + } + }); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = multi.Read( + (a, b, c, d) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}", + splitOn: "BId,CId,DId" + ).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4", results[0]); + } + + // ── 5-type Read multimap ────────────────────────────────────── + + [Fact] + public void GridReader_Read_5Types_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, + { "DId", 4 }, + { "EId", 5 } + } + }); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = multi.Read( + (a, b, c, d, e) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}-{e.EId}", + splitOn: "BId,CId,DId,EId" + ).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4-5", results[0]); + } + + // ── 6-type Read multimap ────────────────────────────────────── + + [Fact] + public void GridReader_Read_6Types_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, + { "DId", 4 }, + { "EId", 5 }, + { "FId", 6 } + } + }); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = multi.Read( + (a, b, c, d, e, f) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}-{e.EId}-{f.FId}", + splitOn: "BId,CId,DId,EId,FId" + ).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4-5-6", results[0]); + } + + // ── 7-type Read multimap ────────────────────────────────────── + + [Fact] + public void GridReader_Read_7Types_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "X" }, + { "BId", 2 }, { "BName", "B" }, + { "CId", 3 }, + { "DId", 4 }, + { "EId", 5 }, + { "FId", 6 }, + { "GId", 7 } + } + }); + + using var multi = conn.QueryMultiple("SELECT ..."); + var results = multi.Read( + (a, b, c, d, e, f, g) => $"{a.Id}-{b.BId}-{c.CId}-{d.DId}-{e.EId}-{f.FId}-{g.GId}", + splitOn: "BId,CId,DId,EId,FId,GId" + ).ToList(); + + Assert.Single(results); + Assert.Equal("1-2-3-4-5-6-7", results[0]); + } + + // ── Read(buffered: false) for sync ───────────────────────── + + [Fact] + public void GridReader_Read_Unbuffered_Sync_Works() + { + using var conn = MakeConn(new[] + { + new Dictionary { { "Id", 10 }, { "Name", "Grace" } }, + new Dictionary { { "Id", 11 }, { "Name", "Heidi" } }, + }); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM T"); + var results = multi.Read(buffered: false).ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal(10, results[0].Id); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.GridReaderUnbuffered.cs b/tests/Dapper.Tests/FakeDbTests.GridReaderUnbuffered.cs new file mode 100644 index 000000000..d7c914c0e --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.GridReaderUnbuffered.cs @@ -0,0 +1,307 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional GridReader tests covering unbuffered async reads and more edge cases. + /// + public class FakeDbGridReaderUnbufferedTests + { + private class Item { public int Id { get; set; } public string? Name { get; set; } } + private class Extra { public int Count { get; set; } } + + // ── ReadUnbufferedAsync ─────────────────────────────────────── + + [Fact] + public async Task GridReader_ReadUnbufferedAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + + var results = new List(); + await foreach (var item in multi.ReadUnbufferedAsync()) + { + results.Add(item); + } + + Assert.Equal(2, results.Count); + Assert.Equal("Alice", results[0].Name); + Assert.Equal("Bob", results[1].Name); + } + + [Fact] + public async Task GridReader_ReadUnbufferedAsync_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 99 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + + var results = new List(); + await foreach (var item in multi.ReadUnbufferedAsync()) + { + results.Add(item); + } + + Assert.Single(results); + Assert.Equal(99, (int)results[0].Id); + } + + // ── ReadAsync (buffered) ────────────────────────────────────── + + [Fact] + public async Task GridReader_ReadAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var results = (await multi.ReadAsync()).ToList(); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task GridReader_ReadFirstAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Dave" } }, + new Dictionary { { "Id", 6 }, { "Name", "Eve" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = await multi.ReadFirstAsync(); + + Assert.Equal(5, item.Id); + } + + [Fact] + public async Task GridReader_ReadFirstOrDefaultAsync_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(System.Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = await multi.ReadFirstOrDefaultAsync(); + + Assert.Null(item); + } + + [Fact] + public async Task GridReader_ReadSingleAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Frank" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = await multi.ReadSingleAsync(); + + Assert.Equal(7, item.Id); + } + + [Fact] + public async Task GridReader_ReadSingleOrDefaultAsync_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(System.Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = await multi.ReadSingleOrDefaultAsync(); + + Assert.Null(item); + } + + // ── DisposeAsync ────────────────────────────────────────────── + + [Fact] + public async Task GridReader_DisposeAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + + await using var multi = conn.QueryMultiple("SELECT Id FROM T"); + // Just verify DisposeAsync doesn't throw + } + + // ── Sync Read variants ──────────────────────────────────────── + + [Fact] + public void GridReader_ReadFirst_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 10 }, { "Name", "Grace" } }, + new Dictionary { { "Id", 11 }, { "Name", "Heidi" } }, + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = multi.ReadFirst(); + + Assert.Equal(10, item.Id); + } + + [Fact] + public void GridReader_ReadFirstOrDefault_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(System.Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = multi.ReadFirstOrDefault(); + + Assert.Null(item); + } + + [Fact] + public void GridReader_ReadSingle_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 20 }, { "Name", "Ivan" } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = multi.ReadSingle(); + + Assert.Equal(20, item.Id); + } + + [Fact] + public void GridReader_ReadSingleOrDefault_EmptySet_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(System.Array.Empty>()); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id, Name FROM Items"); + var item = multi.ReadSingleOrDefault(); + + Assert.Null(item); + } + + [Fact] + public void GridReader_ReadDynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 30 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var results = multi.Read().ToList(); + + Assert.Single(results); + Assert.Equal(30, (int)results[0].Id); + } + + [Fact] + public void GridReader_ReadFirstDynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 40 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var item = multi.ReadFirst(); + + Assert.Equal(40, (int)item.Id); + } + + [Fact] + public void GridReader_ReadSingleDynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 50 } } + }); + conn.Open(); + + using var multi = conn.QueryMultiple("SELECT Id FROM T"); + var item = multi.ReadSingle(); + + Assert.Equal(50, (int)item.Id); + } + + // ── QueryMultipleAsync ──────────────────────────────────────── + + [Fact] + public async Task QueryMultipleAsync_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + await using var multi = await conn.QueryMultipleAsync("SELECT Id, Name FROM Items"); + var results = (await multi.ReadAsync()).ToList(); + + Assert.Single(results); + Assert.Equal("Alice", results[0].Name); + } + + [Fact] + public async Task QueryMultipleAsync_WithParams_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Dave" } } + }); + conn.Open(); + + await using var multi = await conn.QueryMultipleAsync( + "SELECT Id, Name FROM Items WHERE Id = @id", new { id = 5 }); + var results = (await multi.ReadAsync()).ToList(); + + Assert.Single(results); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Misc.cs b/tests/Dapper.Tests/FakeDbTests.Misc.cs new file mode 100644 index 000000000..81117ec94 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Misc.cs @@ -0,0 +1,268 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbMiscTests + { + // ── PurgeQueryCache ─────────────────────────────────────────── + + [Fact] + public void PurgeQueryCache_DoesNotThrow() + { + SqlMapper.PurgeQueryCache(); + } + + // ── CommandDefinition flags ─────────────────────────────────── + + [Fact] + public void CommandDefinition_BufferedFalse_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } } + }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ...", flags: CommandFlags.None); + var result = conn.Query(cmd).ToList(); + Assert.Single(result); + } + + [Fact] + public void CommandDefinition_WithCommandTimeout_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var cmd = new CommandDefinition("DELETE FROM T", commandTimeout: 30); + conn.Execute(cmd); + } + + [Fact] + public void CommandDefinition_Pipelined_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var cmd = new CommandDefinition("DELETE FROM T", flags: CommandFlags.Pipelined); + conn.Execute(cmd); + } + + [Fact] + public void CommandDefinition_NoCache_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 }, { "Name", "A" } } }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ...", flags: CommandFlags.NoCache); + var result = conn.Query(cmd).ToList(); + Assert.Single(result); + } + + // ── Query with CommandType.StoredProcedure ───────────────── + + [Fact] + public void Execute_StoredProcedure_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + conn.Execute("MyProc", new { id = 1 }, commandType: CommandType.StoredProcedure); + } + + [Fact] + public void Query_StoredProcedure_ReturnsRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var result = conn.Query("GetUsers", + commandType: CommandType.StoredProcedure).ToList(); + + Assert.Single(result); + } + + // ── Query returns IEnumerable typed ────────────────── + + [Fact] + public void Query_WithType_ReturnsObjects() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var result = conn.Query(typeof(User), "SELECT Id, Name FROM Users").ToList(); + + Assert.Single(result); + Assert.IsType(result[0]); + } + + // ── async versions of misc tests ────────────────────────────── + + [Fact] + public async Task ExecuteAsync_StoredProcedure_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + await conn.ExecuteAsync("MyProc", new { id = 1 }, commandType: CommandType.StoredProcedure); + } + + [Fact] + public async Task QueryAsync_WithType_ReturnsObjects() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 2 }, { "Name", "Bob" } } + }); + conn.Open(); + + var result = (await conn.QueryAsync(typeof(User), "SELECT Id, Name FROM Users")).ToList(); + + Assert.Single(result); + Assert.IsType(result[0]); + } + + [Fact] + public async Task QueryAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 }, { "Name", "A" } } }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ...", flags: CommandFlags.NoCache); + var result = (await conn.QueryAsync(cmd)).ToList(); + Assert.Single(result); + } + + [Fact] + public async Task QueryFirstAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 3 }, { "Name", "C" } } }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var result = await conn.QueryFirstAsync(cmd); + Assert.Equal(3, result.Id); + } + + [Fact] + public async Task QueryFirstOrDefaultAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var result = await conn.QueryFirstOrDefaultAsync(cmd); + Assert.Null(result); + } + + [Fact] + public async Task QuerySingleAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 4 }, { "Name", "D" } } }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var result = await conn.QuerySingleAsync(cmd); + Assert.Equal(4, result.Id); + } + + [Fact] + public async Task QuerySingleOrDefaultAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var cmd = new CommandDefinition("SELECT ..."); + var result = await conn.QuerySingleOrDefaultAsync(cmd); + Assert.Null(result); + } + + [Fact] + public async Task ExecuteScalarAsync_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(42L); + conn.Open(); + + var cmd = new CommandDefinition("SELECT COUNT(*) FROM T"); + Assert.Equal(42L, await conn.ExecuteScalarAsync(cmd)); + } + + // ── Multiple executions on same connection ──────────────────── + + [Fact] + public void MultipleQueries_OnSameConnection_Work() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 }, { "Name", "A" } } }); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 2 }, { "Name", "B" } } }); + conn.Open(); + + var first = conn.QueryFirst("SELECT ..."); + var second = conn.QueryFirst("SELECT ..."); + + Assert.Equal(1, first.Id); + Assert.Equal(2, second.Id); + } + + // ── Cancellation ────────────────────────────────────────────── + + [Fact] + public async Task QueryAsync_WithCancelledToken_RespectedOnOpen() + { + // Cancellation checked when Dapper tries to open a closed connection + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 } } }); + // conn is NOT opened — Dapper will try to open it + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var cmd = new CommandDefinition("SELECT ...", cancellationToken: cts.Token); + await Assert.ThrowsAnyAsync(() => conn.QueryAsync(cmd)); + } + + [Fact] + public async Task ExecuteAsync_WithCancelledToken_RespectedOnOpen() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + // conn is NOT opened — Dapper will try to open it + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var cmd = new CommandDefinition("DELETE FROM T", cancellationToken: cts.Token); + await Assert.ThrowsAnyAsync(() => conn.ExecuteAsync(cmd)); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.MultiMap.cs b/tests/Dapper.Tests/FakeDbTests.MultiMap.cs new file mode 100644 index 000000000..2170bcfe1 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.MultiMap.cs @@ -0,0 +1,162 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbMultiMapTests + { + // Two-type split: columns before splitOn go to TFirst, from splitOn onward to TSecond. + + private class Owner { public int Id { get; set; } public string? Name { get; set; } } + private class Pet { public int PetId { get; set; } public string? Breed { get; set; } } + + [Fact] + public void Query_TwoTypeSplit_MapsCorrectly() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "PetId", 10 }, { "Breed", "Labrador" } + } + }); + conn.Open(); + + var result = conn.Query( + "SELECT o.Id, o.Name, p.PetId, p.Breed FROM Owners o JOIN Pets p ON ...", + (owner, pet) => (owner, pet), + splitOn: "PetId").ToList(); + + Assert.Single(result); + Assert.Equal("Alice", result[0].Item1.Name); + Assert.Equal("Labrador", result[0].Item2.Breed); + } + + [Fact] + public void Query_TwoTypeSplit_MultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" }, { "PetId", 10 }, { "Breed", "Lab" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" }, { "PetId", 20 }, { "Breed", "Pug" } }, + }); + conn.Open(); + + var result = conn.Query( + "SELECT ...", + (o, p) => (o, p), + splitOn: "PetId").ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal("Alice", result[0].Item1.Name); + Assert.Equal("Bob", result[1].Item1.Name); + } + + private class Tag { public int TagId { get; set; } public string? Label { get; set; } } + + [Fact] + public void Query_ThreeTypeSplit_MapsCorrectly() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 1 }, { "Name", "Alice" }, + { "PetId", 10 }, { "Breed", "Lab" }, + { "TagId", 100 }, { "Label", "Vaccinated" } + } + }); + conn.Open(); + + var result = conn.Query( + "SELECT ...", + (o, p, t) => (o, p, t), + splitOn: "PetId,TagId").ToList(); + + Assert.Single(result); + Assert.Equal("Alice", result[0].Item1.Name); + Assert.Equal("Lab", result[0].Item2.Breed); + Assert.Equal("Vaccinated", result[0].Item3.Label); + } + + [Fact] + public void Query_TwoTypeSplit_ExplicitSplitOn_SingleRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 5 }, { "Name", "Charlie" }, + { "PetId", 50 }, { "Breed", "Beagle" } + } + }); + conn.Open(); + + var result = conn.Query( + "SELECT ...", + (o, p) => $"{o.Name}:{p.Breed}", + splitOn: "PetId").ToList(); + + Assert.Single(result); + Assert.Equal("Charlie:Beagle", result[0]); + } + + // ── async multi-map ─────────────────────────────────────────── + + [Fact] + public async Task QueryAsync_TwoTypeSplit_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 3 }, { "Name", "Dave" }, + { "PetId", 30 }, { "Breed", "Corgi" } + } + }); + conn.Open(); + + var result = (await conn.QueryAsync( + "SELECT ...", + (o, p) => (o, p), + splitOn: "PetId")).ToList(); + + Assert.Single(result); + Assert.Equal("Dave", result[0].Item1.Name); + Assert.Equal("Corgi", result[0].Item2.Breed); + } + + // ── Type[] + Func overload ─────────────────────── + + [Fact] + public void Query_TypeArrayOverload_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Id", 7 }, { "Name", "Eve" }, + { "PetId", 70 }, { "Breed", "Dachshund" } + } + }); + conn.Open(); + + var result = conn.Query<(Owner, Pet)>( + "SELECT ...", + new[] { typeof(Owner), typeof(Pet) }, + objs => ((Owner)objs[0], (Pet)objs[1]), + splitOn: "PetId").ToList(); + + Assert.Single(result); + Assert.Equal("Eve", result[0].Item1.Name); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.OutputParams.cs b/tests/Dapper.Tests/FakeDbTests.OutputParams.cs new file mode 100644 index 000000000..8bb575514 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.OutputParams.cs @@ -0,0 +1,133 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbOutputParamTests + { + // ── ParameterDirection tests ────────────────────────────────── + // fakeDb does not write actual output values back to command params, + // so we test the DynamicParameters wiring rather than value round-trips. + + [Fact] + public void DynamicParameters_OutputParam_IsAddedToParameterNames() + { + var dp = new DynamicParameters(); + dp.Add("@newId", dbType: DbType.Int32, direction: ParameterDirection.Output); + + // DynamicParameters.Clean() strips @ prefix internally + Assert.Contains("newId", dp.ParameterNames); + } + + [Fact] + public void DynamicParameters_ReturnValueParam_IsAddedToParameterNames() + { + var dp = new DynamicParameters(); + dp.Add("@ret", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue); + + Assert.Contains("ret", dp.ParameterNames); + } + + [Fact] + public void DynamicParameters_InputOutputParam_IsAddedToParameterNames() + { + var dp = new DynamicParameters(); + dp.Add("@count", value: 0, dbType: DbType.Int32, direction: ParameterDirection.InputOutput); + + Assert.Contains("count", dp.ParameterNames); + } + + [Fact] + public void DynamicParameters_Get_ReturnsNull_ForOutputParamWithDBNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + var dp = new DynamicParameters(); + dp.Add("@newId", dbType: DbType.Int32, direction: ParameterDirection.Output); + + conn.Execute("EXEC CreateUser @newId OUTPUT", dp); + + // fakeDb doesn't set output values; Get returns null for DBNull + var val = dp.Get("@newId"); + Assert.Null(val); + } + + [Fact] + public async Task ExecuteAsync_WithOutputParam_CommandExecutes() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var dp = new DynamicParameters(); + dp.Add("@result", dbType: DbType.Int32, direction: ParameterDirection.Output); + + // Just ensure execution doesn't throw + await conn.ExecuteAsync("EXEC GetCount @result OUTPUT", dp); + } + + [Fact] + public void DynamicParameters_MultipleOutputParams_AllInParameterNames() + { + var dp = new DynamicParameters(); + dp.Add("@id", dbType: DbType.Int32, direction: ParameterDirection.Output); + dp.Add("@name", dbType: DbType.String, direction: ParameterDirection.Output, size: 100); + + var names = dp.ParameterNames.ToList(); + // DynamicParameters.Clean() strips @ prefix internally + Assert.Contains("id", names); + Assert.Contains("name", names); + } + + // ── Output expression-based binding ──────────────────────── + // Tests that the expression wiring is set up without throwing. + + private class UserResult { public int? NewId { get; set; } } + + [Fact] + public void DynamicParameters_Output_Expression_DoesNotThrow() + { + var target = new UserResult(); + var dp = new DynamicParameters(); + + // Just verify Output sets up the binding without throwing + dp.Output(target, x => x.NewId); + } + + [Fact] + public void DynamicParameters_Output_Expression_ExecutesWithoutError() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var target = new UserResult(); + var dp = new DynamicParameters(); + dp.Output(target, x => x.NewId); + dp.Add("@NewId", dbType: DbType.Int32, direction: ParameterDirection.Output); + + // Execute - fakeDb returns DBNull for output params, and int? accepts null + conn.Execute("EXEC CreateUser @NewId OUTPUT", dp); + } + + [Fact] + public void DynamicParameters_Output_InvalidExpression_Throws() + { + var target = new UserResult(); + var dp = new DynamicParameters(); + + // Non-member expression should throw + Assert.ThrowsAny(() => + dp.Output(target, x => (object?)(x.NewId == null ? 1 : 2))); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Parameters.cs b/tests/Dapper.Tests/FakeDbTests.Parameters.cs new file mode 100644 index 000000000..c95d7c5d9 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Parameters.cs @@ -0,0 +1,104 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbParameterTests + { + [Fact] + public void Execute_WithAnonymousParameters_Succeeds() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var rows = conn.Execute("UPDATE Users SET Name = @name WHERE Id = @id", + new { id = 42, name = "Updated" }); + + Assert.Equal(1, rows); + } + + [Fact] + public void Query_WithAnonymousParameters_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 42 }, { "Name", "Found" } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users WHERE Id = @id", + new { id = 42 }).ToList(); + + Assert.Single(result); + Assert.Equal(42, result[0].Id); + } + + [Fact] + public void Execute_WithDynamicParameters_Succeeds() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + var dp = new DynamicParameters(); + dp.Add("id", 10); + dp.Add("name", "DynUser"); + + var rows = conn.Execute("UPDATE Users SET Name = @name WHERE Id = @id", dp); + + Assert.Equal(1, rows); + } + + [Fact] + public void Query_WithDynamicParameters_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 10 }, { "Name", "DynUser" } } + }); + conn.Open(); + + var dp = new DynamicParameters(); + dp.Add("id", 10); + + var result = conn.Query("SELECT Id, Name FROM Users WHERE Id = @id", dp).ToList(); + + Assert.Single(result); + Assert.Equal(10, result[0].Id); + } + + [Fact] + public void Execute_WithNullParameter_Succeeds() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + var rows = conn.Execute("UPDATE Users SET Name = @name WHERE Id = @id", + new { id = 1, name = (string?)null }); + + Assert.Equal(0, rows); + } + + [Fact] + public void ExecuteScalar_WithParameters_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(1); + conn.Open(); + + var exists = conn.ExecuteScalar( + "SELECT COUNT(*) FROM Users WHERE Id = @id", + new { id = 5 }); + + Assert.Equal(1, exists); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Query.cs b/tests/Dapper.Tests/FakeDbTests.Query.cs new file mode 100644 index 000000000..0677a493d --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Query.cs @@ -0,0 +1,229 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbQueryTests + { + private static fakeDbConnection OpenConnection() + { + var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + return conn; + } + + [Fact] + public void Query_MapsColumnsToProperties() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users").ToList(); + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal("Alice", result[0].Name); + } + + [Fact] + public void Query_ReturnsMultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + new Dictionary { { "Id", 3 }, { "Name", "Carol" } }, + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users").ToList(); + + Assert.Equal(3, result.Count); + Assert.Equal("Alice", result[0].Name); + Assert.Equal("Bob", result[1].Name); + Assert.Equal("Carol", result[2].Name); + } + + [Fact] + public void Query_ReturnsEmpty_WhenNoRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users").ToList(); + + Assert.Empty(result); + } + + [Fact] + public void Query_Dynamic_ReturnsExpandoObjects() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 42 }, { "Name", "Dynamic" } } + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users").ToList(); + + Assert.Single(result); + Assert.Equal(42, (int)result[0].Id); + Assert.Equal("Dynamic", (string)result[0].Name); + } + + [Fact] + public void QueryFirst_ReturnsFirstRow() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 10 }, { "Name", "First" } }, + new Dictionary { { "Id", 20 }, { "Name", "Second" } }, + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT Id, Name FROM Users"); + + Assert.Equal(10, result.Id); + Assert.Equal("First", result.Name); + } + + [Fact] + public void QueryFirst_ThrowsOnEmptyResult() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + Assert.Throws(() => + conn.QueryFirst("SELECT Id, Name FROM Users")); + } + + [Fact] + public void QueryFirstOrDefault_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var result = conn.QueryFirstOrDefault("SELECT Id, Name FROM Users"); + + Assert.Null(result); + } + + [Fact] + public void QueryFirstOrDefault_ReturnsFirstRow_WhenMultiple() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + var result = conn.QueryFirstOrDefault("SELECT Id, Name FROM Users"); + + Assert.NotNull(result); + Assert.Equal(1, result!.Id); + } + + [Fact] + public void QuerySingle_ReturnsRow_WhenExactlyOne() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Solo" } } + }); + conn.Open(); + + var result = conn.QuerySingle("SELECT Id, Name FROM Users WHERE Id = 7"); + + Assert.Equal(7, result.Id); + Assert.Equal("Solo", result.Name); + } + + [Fact] + public void QuerySingle_ThrowsOnEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + Assert.Throws(() => + conn.QuerySingle("SELECT Id, Name FROM Users WHERE Id = 99")); + } + + [Fact] + public void QuerySingle_ThrowsOnMultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + Assert.Throws(() => + conn.QuerySingle("SELECT Id, Name FROM Users")); + } + + [Fact] + public void QuerySingleOrDefault_ReturnsNull_WhenEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + + var result = conn.QuerySingleOrDefault("SELECT Id, Name FROM Users WHERE Id = 99"); + + Assert.Null(result); + } + + [Fact] + public void Query_MapsNullableColumnToNullProperty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", DBNull.Value } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT Id, Name FROM Users WHERE Id = 5"); + + Assert.Equal(5, result.Id); + Assert.Null(result.Name); + } + + [Fact] + public void Query_IsCaseInsensitive_ForColumnNames() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "id", 3 }, { "name", "Lower" } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT id, name FROM Users"); + + Assert.Equal(3, result.Id); + Assert.Equal("Lower", result.Name); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.ReaderParser.cs b/tests/Dapper.Tests/FakeDbTests.ReaderParser.cs new file mode 100644 index 000000000..8b35e7d63 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.ReaderParser.cs @@ -0,0 +1,155 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for Parse extension methods on IDataReader and GetRowParser. + /// These cover the SqlMapper.IDataReader.cs partial class. + /// + public class FakeDbReaderParserTests + { + private class User { public int Id { get; set; } public string? Name { get; set; } } + + // ── Parse(IDataReader) ───────────────────────────────────── + + [Fact] + public void IDataReader_Parse_Generic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var results = reader.Parse().ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal("Alice", results[0].Name); + Assert.Equal("Bob", results[1].Name); + } + + [Fact] + public void IDataReader_Parse_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Charlie" } } + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var results = reader.Parse(typeof(User)).ToList(); + + Assert.Single(results); + Assert.Equal(5, ((User)results[0]).Id); + } + + [Fact] + public void IDataReader_Parse_Dynamic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Dave" } } + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var results = reader.Parse().ToList(); + + Assert.Single(results); + Assert.Equal(7, (int)results[0].Id); + } + + // ── GetRowParser(IDataReader) ────────────────────────────── + + [Fact] + public void IDataReader_GetRowParser_Generic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Eve" } } + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var parser = reader.GetRowParser(); + + Assert.True(reader.Read()); + var user = parser(reader); + Assert.Equal(3, user.Id); + Assert.Equal("Eve", user.Name); + } + + [Fact] + public void IDataReader_GetRowParser_ByType_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 9 }, { "Name", "Frank" } } + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + var parser = reader.GetRowParser(typeof(User)); + + Assert.True(reader.Read()); + var user = parser(reader); + Assert.Equal(9, user.Id); + } + + // ── GetRowParser(DbDataReader) ───────────────────────────── + + [Fact] + public void DbDataReader_GetRowParser_Generic_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 11 }, { "Name", "Grace" } } + }); + conn.Open(); + + using var dbReader = (System.Data.Common.DbDataReader)conn.ExecuteReader("SELECT Id, Name FROM Users"); + var parser = dbReader.GetRowParser(); + + Assert.True(dbReader.Read()); + var user = parser(dbReader); + Assert.Equal(11, user.Id); + } + + // ── Parse with scalar types ──────────────────────────────── + + [Fact] + public void IDataReader_Parse_Scalar_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", 42 } }, + new Dictionary { { "Val", 43 } }, + }); + conn.Open(); + + using IDataReader reader = conn.ExecuteReader("SELECT Val FROM T"); + var results = reader.Parse().ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal(42, results[0]); + Assert.Equal(43, results[1]); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Settings.cs b/tests/Dapper.Tests/FakeDbTests.Settings.cs new file mode 100644 index 000000000..d9f7f4f9a --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Settings.cs @@ -0,0 +1,196 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for SqlMapper.Settings properties and LiteralToken ({=param}) substitution. + /// + public class FakeDbSettingsTests + { + // ── SqlMapper.Settings ──────────────────────────────────────── + + [Fact] + public void Settings_CommandTimeout_CanBeSet() + { + var original = SqlMapper.Settings.CommandTimeout; + try + { + SqlMapper.Settings.CommandTimeout = 60; + Assert.Equal(60, SqlMapper.Settings.CommandTimeout); + } + finally + { + SqlMapper.Settings.CommandTimeout = original; + } + } + + [Fact] + public void Settings_CommandTimeout_NullAllowed() + { + var original = SqlMapper.Settings.CommandTimeout; + try + { + SqlMapper.Settings.CommandTimeout = null; + Assert.Null(SqlMapper.Settings.CommandTimeout); + } + finally + { + SqlMapper.Settings.CommandTimeout = original; + } + } + + [Fact] + public void Settings_ApplyNullValues_CanBeSet() + { + var original = SqlMapper.Settings.ApplyNullValues; + try + { + SqlMapper.Settings.ApplyNullValues = true; + Assert.True(SqlMapper.Settings.ApplyNullValues); + SqlMapper.Settings.ApplyNullValues = false; + Assert.False(SqlMapper.Settings.ApplyNullValues); + } + finally + { + SqlMapper.Settings.ApplyNullValues = original; + } + } + + [Fact] + public void Settings_PadListExpansions_CanBeSet() + { + var original = SqlMapper.Settings.PadListExpansions; + try + { + SqlMapper.Settings.PadListExpansions = false; + Assert.False(SqlMapper.Settings.PadListExpansions); + SqlMapper.Settings.PadListExpansions = true; + Assert.True(SqlMapper.Settings.PadListExpansions); + } + finally + { + SqlMapper.Settings.PadListExpansions = original; + } + } + + [Fact] + public void Settings_UseIncrementalPseudoPositionalParameterNames_CanBeSet() + { + var original = SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames; + try + { + SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames = true; + Assert.True(SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames); + } + finally + { + SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames = original; + } + } + + // ── Literal token {=param} substitution ────────────────────── + + private class SimpleResult { public int Val { get; set; } } + + [Fact] + public void Query_LiteralToken_IntValue_Substituted() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", 42 } } + }); + conn.Open(); + + // {=val} is a literal substitution — the value is embedded directly in the SQL + var result = conn.Query( + "SELECT {=val} AS Val", new { val = 42 }).ToList(); + + Assert.Single(result); + Assert.Equal(42, result[0].Val); + } + + [Fact] + public void Query_LiteralToken_StringValue_Substituted() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", 99 } } + }); + conn.Open(); + + var result = conn.Query( + "SELECT {=n} AS Val FROM T WHERE n = {=n}", new { n = 99 }).ToList(); + + Assert.Single(result); + } + + // ── PropertyInfoByNameComparer (used by DefaultTypeMap) ─────── + + [Fact] + public void DefaultTypeMap_GetMember_CaseInsensitive_Works() + { + // DefaultTypeMap uses PropertyInfoByNameComparer for case-insensitive matching + var map = new DefaultTypeMap(typeof(SimpleResult)); + var member = map.GetMember("val"); // lowercase - should match Val property + Assert.NotNull(member); + } + + // ── Identity and caching ────────────────────────────────────── + + [Fact] + public void PurgeQueryCache_ReducesCacheCount() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", 1 } } + }); + conn.Open(); + + // Execute a query to populate cache + conn.Query("SELECT 1 AS Val").ToList(); + var before = SqlMapper.GetCachedSQL(); + SqlMapper.PurgeQueryCache(); + var after = SqlMapper.GetCachedSQL(); + + // After purge, cache should be empty + Assert.Equal(0, after.Count()); + } + + // ── Settings.ApplyNullValues behavior ───────────────────────── + + private class NullableResult { public int? Value { get; set; } } + + [Fact] + public void Query_WithApplyNullValues_SetsNullProperty() + { + var original = SqlMapper.Settings.ApplyNullValues; + try + { + SqlMapper.Settings.ApplyNullValues = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Value", DBNull.Value } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT NULL AS Value"); + Assert.Null(result.Value); + } + finally + { + SqlMapper.Settings.ApplyNullValues = original; + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.SqlMapperAdvanced.cs b/tests/Dapper.Tests/FakeDbTests.SqlMapperAdvanced.cs new file mode 100644 index 000000000..c1d3b9ddf --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.SqlMapperAdvanced.cs @@ -0,0 +1,628 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +#pragma warning disable CS0618 // Obsolete internal-use-only members are intentionally exercised here + +namespace Dapper.Tests +{ + // ── PassByPosition (L1900-1944) ──────────────────────────────────────────── + + /// + /// Covers ShouldPassByPosition + PassByPosition: SQL with ?x? pseudo-positional params. + /// + public class FakeDbSqlMapperPassByPositionTests + { + [Fact] + public void PassByPosition_BasicQuery_RewritesSqlAndExecutes() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 3 } } }); + conn.Open(); + // ?x? and ?y? trigger ShouldPassByPosition → PassByPosition + var result = conn.QueryFirst("SELECT ?x? + ?y? AS v", new { x = 1, y = 2 }); + Assert.Equal(3, result); + } + + [Fact] + public void PassByPosition_UnknownParam_LeavesAlone() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 5 } } }); + conn.Open(); + // ?z? is not in the param object → leaves token as-is (L1940-1942) + var result = conn.QueryFirst("SELECT ?x? + ?z? AS v", new { x = 1, y = 2 }); + Assert.Equal(5, result); + } + + [Fact] + public void PassByPosition_IncrementalNames_SetsParameterNames() + { + var original = SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames; + try + { + SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames = true; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 10 } } }); + conn.Open(); + var result = conn.QueryFirst("SELECT ?x? AS v", new { x = 10 }); + Assert.Equal(10, result); + } + finally + { + SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames = original; + } + } + + [Fact] + public void PassByPosition_DuplicateParamReference_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + // Using ?x? twice should throw because once consumed, second reference fails + Assert.Throws(() => + conn.QueryFirst("SELECT ?x? + ?x? AS v", new { x = 1 })); + } + } + + // ── Empty IN-list handling (L2255-2274) ──────────────────────────────────── + + /// + /// Covers the empty-list path in PackListParameters: rewrites IN @ids to (SELECT @ids WHERE 1=0). + /// + public class FakeDbSqlMapperEmptyListTests + { + [Fact] + public void Query_EmptyIntList_ReturnsEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var results = conn.Query( + "SELECT Id FROM T WHERE Id IN @ids", + new { ids = Array.Empty() }).ToList(); + Assert.Empty(results); + } + + [Fact] + public void Query_EmptyLongList_ReturnsEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var results = conn.Query( + "SELECT Id FROM T WHERE Id IN @ids", + new { ids = Array.Empty() }).ToList(); + Assert.Empty(results); + } + + [Fact] + public void Query_EmptyStringList_ReturnsEmpty() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var results = conn.Query( + "SELECT Name FROM T WHERE Name IN @names", + new { names = Array.Empty() }).ToList(); + Assert.Empty(results); + } + } + + // ── PadListExpansions (L2229-2245) ───────────────────────────────────────── + + /// + /// Covers the PadListExpansions=true code path: pads expanded IN-list parameters. + /// + public class FakeDbSqlMapperPadListExpansionsTests + { + [Fact] + public void PadListExpansions_ListOf7_PadsTo10() + { + var original = SqlMapper.Settings.PadListExpansions; + try + { + SqlMapper.Settings.PadListExpansions = true; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new[] { 1, 2, 3, 4, 5, 6, 7 }; + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + // If no exception thrown, padding executed correctly + } + finally + { + SqlMapper.Settings.PadListExpansions = original; + } + } + + [Fact] + public void PadListExpansions_StringList_PadsCorrectly() + { + var original = SqlMapper.Settings.PadListExpansions; + try + { + SqlMapper.Settings.PadListExpansions = true; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var names = new[] { "a", "b", "c", "d", "e", "f", "g" }; + conn.Query("SELECT Name FROM T WHERE Name IN @names", new { names }).ToList(); + } + finally + { + SqlMapper.Settings.PadListExpansions = original; + } + } + } + + // ── GetListPaddingExtraCount (L2119-2144) ────────────────────────────────── + + /// + /// Directly covers all branches of GetListPaddingExtraCount internal method. + /// + public class FakeDbSqlMapperPaddingCountTests + { + [Theory] + [InlineData(0, 0)] + [InlineData(1, 0)] + [InlineData(5, 0)] + [InlineData(-1, 0)] + [InlineData(6, 4)] // 6 % 10 = 6 → need 4 more + [InlineData(10, 0)] // 10 % 10 = 0 → no padding + [InlineData(17, 3)] // 17 % 10 = 7 → need 3 more + [InlineData(150, 0)] // 150 % 10 = 0 → no padding (boundary) + [InlineData(151, 49)] // padFactor=50; 151 % 50 = 1 → need 49 more + [InlineData(750, 0)] // 750 % 50 = 0 → no padding + [InlineData(751, 49)] // padFactor=100; 751 % 100 = 51 → need 49 more + [InlineData(2000, 0)] // boundary of padFactor=100 + [InlineData(2001, 9)] // padFactor=10; 2001 % 10 = 1 → need 9 more + [InlineData(2070, 0)] // boundary of padFactor=10 + [InlineData(2071, 0)] // between 2070-2100 → return 0 + [InlineData(2100, 0)] // boundary between 2070-2100 → return 0 + [InlineData(2101, 99)] // padFactor=200; 2101 % 200 = 101 → need 99 more + public void GetListPaddingExtraCount_ReturnsExpected(int count, int expected) + { + var result = SqlMapper.GetListPaddingExtraCount(count); + Assert.Equal(expected, result); + } + } + + // ── TryStringSplit (L2310-2376) ──────────────────────────────────────────── + + /// + /// Covers TryStringSplit dispatch (int/long/short/byte) and TryStringSplit<T> body. + /// Triggered by Settings.InListStringSplitCount >= 0 with lists at or above the threshold. + /// + public class FakeDbSqlMapperStringSplitTests + { + [Fact] + public void InListStringSplit_IntList_AtThreshold_UsesSplit() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 5; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new[] { 1, 2, 3, 4, 5, 6 }; // 6 >= 5 + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void InListStringSplit_LongList_AtThreshold_UsesSplit() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 3; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new long[] { 10L, 20L, 30L, 40L }; // 4 >= 3 + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void InListStringSplit_ShortList_AtThreshold_UsesSplit() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 2; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new short[] { 1, 2, 3 }; // 3 >= 2 + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void InListStringSplit_ByteList_AtThreshold_UsesSplit() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 2; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new byte[] { 1, 2, 3 }; // 3 >= 2 + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void InListStringSplit_BelowThreshold_ExpandsNormally() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 10; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new[] { 1, 2, 3 }; // 3 < 10 → normal expansion + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void InListStringSplit_SingleItem_IterPath_Works() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 0; // 1 >= 0 + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + var ids = new[] { 42 }; + conn.Query("SELECT Id FROM T WHERE Id IN @ids", new { ids }).ToList(); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + } + + // ── Format() method (L2436-2502) ────────────────────────────────────────── + + /// + /// Directly covers Format() all TypeCode branches, the multiexec (IEnumerable) path, + /// empty multiexec, and the unsupported type exception. + /// + public class FakeDbSqlMapperFormatTests + { + [Fact] public void Format_Null_ReturnsNullString() => Assert.Equal("null", SqlMapper.Format(null)); + [Fact] public void Format_DBNull_ReturnsNullString() => Assert.Equal("null", SqlMapper.Format(DBNull.Value)); + [Fact] public void Format_True_Returns1() => Assert.Equal("1", SqlMapper.Format(true)); + [Fact] public void Format_False_Returns0() => Assert.Equal("0", SqlMapper.Format(false)); + [Fact] public void Format_Byte_ReturnsString() => Assert.Equal("5", SqlMapper.Format((byte)5)); + [Fact] public void Format_SByte_ReturnsString() => Assert.Equal("-3", SqlMapper.Format((sbyte)-3)); + [Fact] public void Format_UInt16_ReturnsString() => Assert.Equal("65535", SqlMapper.Format((ushort)65535)); + [Fact] public void Format_Int16_ReturnsString() => Assert.Equal("-100", SqlMapper.Format((short)-100)); + [Fact] public void Format_UInt32_ReturnsString() => Assert.Equal("4000000000", SqlMapper.Format((uint)4_000_000_000u)); + [Fact] public void Format_Int32_ReturnsString() => Assert.Equal("42", SqlMapper.Format((int)42)); + [Fact] public void Format_UInt64_ReturnsString() => Assert.Equal("18446744073709551615", SqlMapper.Format(ulong.MaxValue)); + [Fact] public void Format_Int64_ReturnsString() => Assert.Equal("-9223372036854775808", SqlMapper.Format(long.MinValue)); + [Fact] public void Format_Single_ReturnsString() => Assert.Equal("1.5", SqlMapper.Format((float)1.5f)); + [Fact] public void Format_Double_ReturnsString() => Assert.Equal("3.14", SqlMapper.Format((double)3.14)); + [Fact] public void Format_Decimal_ReturnsString() => Assert.Equal("123.456", SqlMapper.Format((decimal)123.456m)); + + [Fact] + public void Format_IntArray_NonEmpty_ReturnsTuple() + { + var result = SqlMapper.Format(new[] { 1, 2, 3 }); + Assert.Equal("(1,2,3)", result); + } + + [Fact] + public void Format_IntList_NonEmpty_ReturnsTuple() + { + var result = SqlMapper.Format(new List { 10, 20 }); + Assert.Equal("(10,20)", result); + } + + [Fact] + public void Format_EmptyArray_ReturnsSelectNull() + { + var result = SqlMapper.Format(Array.Empty()); + Assert.Equal("(select null where 1=0)", result); + } + + [Fact] + public void Format_UnsupportedType_Throws() + { + Assert.Throws(() => SqlMapper.Format(new object())); + } + } + + // ── ReadChar / ReadNullableChar (L2071-2092) ─────────────────────────────── + + /// + /// Covers ReadChar and ReadNullableChar all branches. + /// + public class FakeDbSqlMapperReadCharTests + { + [Fact] + public void ReadChar_SingleCharString_ReturnsChar() + { + var c = SqlMapper.ReadChar("a"); + Assert.Equal('a', c); + } + + [Fact] + public void ReadChar_CharValue_ReturnsChar() + { + var c = SqlMapper.ReadChar('z'); + Assert.Equal('z', c); + } + + [Fact] + public void ReadChar_Null_Throws() + { + Assert.Throws(() => SqlMapper.ReadChar(null!)); + } + + [Fact] + public void ReadChar_DBNull_Throws() + { + Assert.Throws(() => SqlMapper.ReadChar(DBNull.Value)); + } + + [Fact] + public void ReadChar_MultiCharString_Throws() + { + Assert.Throws(() => SqlMapper.ReadChar("ab")); + } + + [Fact] + public void ReadNullableChar_Null_ReturnsNull() + { + var c = SqlMapper.ReadNullableChar(null!); + Assert.Null(c); + } + + [Fact] + public void ReadNullableChar_DBNull_ReturnsNull() + { + var c = SqlMapper.ReadNullableChar(DBNull.Value); + Assert.Null(c); + } + + [Fact] + public void ReadNullableChar_SingleCharString_ReturnsChar() + { + var c = SqlMapper.ReadNullableChar("x"); + Assert.Equal('x', c); + } + + [Fact] + public void ReadNullableChar_CharValue_ReturnsChar() + { + var c = SqlMapper.ReadNullableChar('k'); + Assert.Equal('k', c); + } + + [Fact] + public void ReadNullableChar_MultiCharString_Throws() + { + Assert.Throws(() => SqlMapper.ReadNullableChar("ab")); + } + } + + // ── ReplaceLiterals via {=col} literal token SQL syntax (L2505-2517) ─────── + + /// + /// Covers ReplaceLiterals: Dapper substitutes {=col} tokens with SQL literal values. + /// Simultaneously exercises Format() for various types via the {=x} path. + /// + public class FakeDbSqlMapperReplaceLiteralsTests + { + [Fact] + public void LiteralToken_Int_SubstitutedInSQL() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 42 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + var result = conn.QueryFirst("SELECT {=x} AS v FROM T_LitInt", new { x = 42 }); + Assert.Equal(42, result); + } + + [Fact] + public void LiteralToken_Bool_SubstitutedInSQL() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + var result = conn.QueryFirst("SELECT {=flag} AS v FROM T_LitBool", new { flag = true }); + Assert.Equal(1, result); + } + + [Fact] + public void LiteralToken_Long_SubstitutedInSQL() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + var result = conn.QueryFirst("SELECT {=id} AS v FROM T_LitLong", new { id = 100L }); + Assert.Equal(1, result); + } + } + + // ── Dynamic multimap (L1747-1773) ───────────────────────────────────────── + + /// + /// Covers GenerateDeserializers dynamic path: first type is object/dynamic. + /// + public class FakeDbSqlMapperDynamicMultimapTests + { + [Fact] + public void DynamicMultimap_TwoTypes_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Name", "Alice" }, { "Id", 1 }, { "Score", 99 } } + }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + var results = conn.Query( + "SELECT Name, Id, Score FROM T_DynMulti", + (a, b) => new AdvancedResult { DynamicName = ((IDictionary)a)["Name"]?.ToString(), Score = b.Score }, + splitOn: "Id").ToList(); + + Assert.Single(results); + Assert.Equal("Alice", results[0].DynamicName); + } + + [Fact] + public void DynamicMultimap_SplitOnStar_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Name", "Bob" }, { "Score", 42 } } + }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // splitOn="*" → each column gets its own deserializer slot + var results = conn.Query( + "SELECT Name, Score FROM T_DynMultiStar", + (a, b) => new AdvancedResult { DynamicName = "ok", Score = 0 }, + splitOn: "*").ToList(); + + Assert.Single(results); + } + } + + // ── MultiMapException paths (L1984-2001) ─────────────────────────────────── + + /// + /// Covers MultiMapException: called when splitOn column not found or no columns. + /// + public class FakeDbSqlMapperMultiMapExceptionTests + { + [Fact] + public void MultiMap_BadSplitOn_ThrowsArgumentException() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "X" } } + }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // splitOn="MissingColumn" — column doesn't exist → MultiMapException + Assert.Throws(() => + conn.Query( + "SELECT Id, Name FROM T_BadSplitOn", + (a, b) => new AdvancedResult(), + splitOn: "MissingColumn").ToList()); + } + + [Fact] + public void MultiMap_NoSplitOnSpecified_ThrowsArgumentException() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Name", "X" }, { "Score", 1 } } + }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // Default splitOn="Id" — not present in results → MultiMapException with message about splitOn + Assert.Throws(() => + conn.Query( + "SELECT Name, Score FROM T_NoSplitOn", + (a, b) => new AdvancedResult(), + splitOn: "Id").ToList()); + } + } + + // ── GetDapperRowDeserializer startBound != 0 (L2052-2060) ───────────────── + + /// + /// Covers the startBound != 0 path in GetDapperRowDeserializer: triggered by multimap + /// queries where second/later types start mid-reader. + /// + public class FakeDbSqlMapperDapperRowStartBoundTests + { + [Fact] + public void DynamicMultimap_SecondTypeIsDynamic_StartBoundNonZero() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" }, { "Score", 10 } } + }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // First type is typed, second is dynamic → startBound != 0 for second + var results = conn.Query( + "SELECT Id, Name, Score FROM T_StartBound", + (a, b) => new AdvancedResult { Score = a.Id }, + splitOn: "Name").ToList(); + + Assert.Single(results); + } + } + + // ── Helper types ────────────────────────────────────────────────────────── + + internal class AdvancedA + { + public int Id { get; set; } + } + + internal class AdvancedB + { + public int Score { get; set; } + } + + internal class AdvancedResult + { + public string? DynamicName { get; set; } + public int Score { get; set; } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.SqlMapperDirectApi.cs b/tests/Dapper.Tests/FakeDbTests.SqlMapperDirectApi.cs new file mode 100644 index 000000000..a2c28f3fa --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.SqlMapperDirectApi.cs @@ -0,0 +1,382 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +#pragma warning disable CS0618 // Obsolete internal-use-only members are intentionally exercised here + +namespace Dapper.Tests +{ + // ── SanitizeParameterValue with all enum underlying types (L2387-2403) ───── + + /// + /// Covers SanitizeParameterValue with enum types of every underlying numeric type, + /// exercising all TypeCode case branches. + /// + public class FakeDbSanitizeParameterValueTests + { + private enum ByteEnum : byte { A = 1 } + private enum SByteEnum : sbyte { A = 2 } + private enum Int16Enum : short { A = 3 } + private enum Int32Enum : int { A = 4 } + private enum Int64Enum : long { A = 5 } + private enum UInt16Enum : ushort { A = 6 } + private enum UInt32Enum : uint { A = 7 } + private enum UInt64Enum : ulong { A = 8 } + + [Fact] public void Byte_Enum_ReturnsByte() => + Assert.Equal((byte)1, SqlMapper.SanitizeParameterValue(ByteEnum.A)); + [Fact] public void SByte_Enum_ReturnsSByte() => + Assert.Equal((sbyte)2, SqlMapper.SanitizeParameterValue(SByteEnum.A)); + [Fact] public void Int16_Enum_ReturnsShort() => + Assert.Equal((short)3, SqlMapper.SanitizeParameterValue(Int16Enum.A)); + [Fact] public void Int32_Enum_ReturnsInt() => + Assert.Equal((int)4, SqlMapper.SanitizeParameterValue(Int32Enum.A)); + [Fact] public void Int64_Enum_ReturnsLong() => + Assert.Equal((long)5, SqlMapper.SanitizeParameterValue(Int64Enum.A)); + [Fact] public void UInt16_Enum_ReturnsUShort() => + Assert.Equal((ushort)6, SqlMapper.SanitizeParameterValue(UInt16Enum.A)); + [Fact] public void UInt32_Enum_ReturnsUInt() => + Assert.Equal((uint)7, SqlMapper.SanitizeParameterValue(UInt32Enum.A)); + [Fact] public void UInt64_Enum_ReturnsULong() => + Assert.Equal((ulong)8, SqlMapper.SanitizeParameterValue(UInt64Enum.A)); + [Fact] public void Null_ReturnsDBNull() => + Assert.Equal(DBNull.Value, SqlMapper.SanitizeParameterValue(null)); + [Fact] public void NonEnum_ReturnsSelf() => + Assert.Equal("hello", SqlMapper.SanitizeParameterValue("hello")); + } + + // ── QuerySingleOrDefault dynamic overload (L821-822) ────────────────────── + + public class FakeDbQuerySingleOrDefaultDynamicTests + { + [Fact] + public void QuerySingleOrDefault_String_ReturnsDynamic() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 42 } } }); + conn.Open(); + dynamic? result = conn.QuerySingleOrDefault("SELECT v FROM T_DynSoD"); + Assert.NotNull(result); + } + + [Fact] + public void QuerySingleOrDefault_String_NoRows_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + dynamic? result = conn.QuerySingleOrDefault("SELECT v FROM T_DynSoD2"); + Assert.Null(result); + } + } + + // ── ExecuteReader with multi-exec throws (L3067-3068) ──────────────────── + + public class FakeDbExecuteReaderMultiExecTests + { + [Fact] + public void ExecuteReader_WithIEnumerableParam_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + SqlMapper.PurgeQueryCache(); + Assert.Throws(() => + conn.ExecuteReader( + new CommandDefinition("INSERT INTO T VALUES (@id)", + new[] { new { id = 1 }, new { id = 2 } }))); + } + } + + // ── ValueTuple param throws NotSupportedException (L2557-2558) ──────────── + + public class FakeDbValueTupleParamTests + { + [Fact] + public void Query_WithValueTupleParam_ThrowsNotSupported() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + SqlMapper.PurgeQueryCache(); + Assert.Throws(() => + conn.QueryFirst("SELECT @Item1 AS v", (1, 2))); + } + } + + // ── SetTypeMap null throws (L3231) ───────────────────────────────────────── + + public class FakeDbSetTypeMapTests + { + [Fact] + public void SetTypeMap_NullType_ThrowsArgumentNullException() + { + Assert.Throws(() => SqlMapper.SetTypeMap(null!, null)); + } + + [Fact] + public void SetTypeMap_NullMap_RemovesCustomMap() + { + // register then remove + SqlMapper.SetTypeMap(typeof(SetTypeMapHelper), new CustomPropertyTypeMap( + typeof(SetTypeMapHelper), (type, name) => type.GetProperty(name))); + SqlMapper.SetTypeMap(typeof(SetTypeMapHelper), null); // remove + } + } + + // ── ReplaceLiterals extension method (L2424-2426) ───────────────────────── + + public class FakeDbReplaceLiteralsExtensionTests + { + [Fact] + public void ReplaceLiterals_Extension_ReplacesToken() + { + // DynamicParameters implements IParameterLookup + var dp = new DynamicParameters(); + dp.Add("x", 42); + + // create a fake command and set sql with literal token + var fakeCmd = new LiteralsTestCommand(); + fakeCmd.CommandText = "SELECT {=x} AS v"; + + // call the extension method + dp.ReplaceLiterals(fakeCmd); + + // {=x} should have been replaced with "42" + Assert.Contains("42", fakeCmd.CommandText); + } + + [Fact] + public void ReplaceLiterals_Extension_NoTokens_NoOp() + { + var dp = new DynamicParameters(); + dp.Add("x", 42); + var fakeCmd = new LiteralsTestCommand(); + fakeCmd.CommandText = "SELECT @x AS v"; // no literal token + + dp.ReplaceLiterals(fakeCmd); + + Assert.Equal("SELECT @x AS v", fakeCmd.CommandText); + } + } + + // ── Pipelined Execute (L638-641) ─────────────────────────────────────────── + + public class FakeDbPipelinedExecuteTests + { + [Fact] + public void Execute_WithPipelinedFlag_AndMultiExec_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.EnqueueNonQueryResult(1); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + var rows = new[] { new { id = 1 }, new { id = 2 } }; + var cmd = new CommandDefinition( + "INSERT INTO T VALUES (@id)", rows, + flags: CommandFlags.Pipelined); + conn.Execute(cmd); + } + } + + // ── DbString list expansion (L2201-2204) ────────────────────────────────── + + public class FakeDbDbStringListTests + { + [Fact] + public void Query_WithDbStringList_ExpandsCorrectly() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + var names = new[] { + new DbString { Value = "Alice", IsFixedLength = false }, + new DbString { Value = "Bob", IsFixedLength = false } + }; + conn.Query("SELECT Name FROM T WHERE Name IN @names", + new { names }).ToList(); + } + } + + // ── Struct param triggers isStruct path (L2579-2582) ────────────────────── + + public class FakeDbStructParamTests + { + [Fact] + public void Query_WithStructParam_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + var result = conn.QueryFirst( + "SELECT @Id AS v FROM T_Struct", + new StructQueryParam { Id = 1 }); + Assert.Equal(1, result); + } + + [Fact] + public void Execute_WithStructParam_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + SqlMapper.PurgeQueryCache(); + conn.Execute("UPDATE T SET v = @Value WHERE Id = @Id", + new StructQueryParam2 { Id = 1, Value = 99 }); + } + } + + // ── Out-of-order ctor params → hard-way sort (L2613-2650) ───────────────── + + /// + /// Tests CreateParamInfoGenerator ctor-sort path: + /// when property declaration order differs from ctor param order, + /// Dapper uses positionByName to re-sort them. + /// + public class FakeDbCtorSortParamTests + { + [Fact] + public void Query_OutOfOrderCtorParam_WorksCorrectly() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // OutOfOrderCtorParam: properties declared B, A but ctor takes (a, b) + // → triggers ok=false at L2613, then hard-way sort at L2625 + var result = conn.QueryFirst( + "SELECT @a + @b AS v FROM T_CtorSort", + new OutOfOrderCtorParam("x", 1)); + Assert.Equal(1, result); + } + + [Fact] + public void Query_MismatchedCtorNames_FallsToAlphabeticalSort() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + SqlMapper.PurgeQueryCache(); + + // MismatchedCtorParam: property C not in ctor → hard-way sort fails → alphabetical sort + var result = conn.QueryFirst( + "SELECT @a + @c AS v FROM T_AlphaSort", + new MismatchedCtorParam(1, 2)); + Assert.Equal(1, result); + } + } + + // ── Helper types ────────────────────────────────────────────────────────── + + internal class SetTypeMapHelper + { + public int Id { get; set; } + } + + internal class LiteralsTestCommand : System.Data.Common.DbCommand + { + public override string CommandText { get; set; } = ""; + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } = CommandType.Text; + public override bool DesignTimeVisible { get; set; } + public override UpdateRowSource UpdatedRowSource { get; set; } + protected override System.Data.Common.DbConnection? DbConnection { get; set; } + protected override System.Data.Common.DbParameterCollection DbParameterCollection { get; } = new LiteralsParamCollection(); + protected override System.Data.Common.DbTransaction? DbTransaction { get; set; } + + public override void Cancel() { } + public override int ExecuteNonQuery() => 0; + public override object? ExecuteScalar() => null; + public override void Prepare() { } + protected override System.Data.Common.DbParameter CreateDbParameter() => new LiteralsParam(); + protected override System.Data.Common.DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotImplementedException(); + } + + internal class LiteralsParam : System.Data.Common.DbParameter + { + public override DbType DbType { get; set; } + public override ParameterDirection Direction { get; set; } + public override bool IsNullable { get; set; } + public override string? ParameterName { get; set; } + public override int Size { get; set; } + public override string? SourceColumn { get; set; } + public override bool SourceColumnNullMapping { get; set; } + public override object? Value { get; set; } + public override void ResetDbType() { } + } + + internal class LiteralsParamCollection : System.Data.Common.DbParameterCollection + { + private readonly System.Collections.ArrayList _list = new(); + public override int Count => _list.Count; + public override object SyncRoot => _list.SyncRoot; + public override int Add(object value) => _list.Add(value); + public override void AddRange(Array values) => _list.AddRange(values); + public override void Clear() => _list.Clear(); + public override bool Contains(object value) => _list.Contains(value); + public override bool Contains(string value) => false; + public override void CopyTo(Array array, int index) => _list.CopyTo(array, index); + public override System.Collections.IEnumerator GetEnumerator() => _list.GetEnumerator(); + public override int IndexOf(object value) => _list.IndexOf(value); + public override int IndexOf(string parameterName) => -1; + public override void Insert(int index, object value) => _list.Insert(index, value); + public override void Remove(object value) => _list.Remove(value); + public override void RemoveAt(int index) => _list.RemoveAt(index); + public override void RemoveAt(string parameterName) { } + protected override System.Data.Common.DbParameter GetParameter(int index) => (System.Data.Common.DbParameter)_list[index]!; + protected override System.Data.Common.DbParameter GetParameter(string parameterName) => throw new NotImplementedException(); + protected override void SetParameter(int index, System.Data.Common.DbParameter value) => _list[index] = value; + protected override void SetParameter(string parameterName, System.Data.Common.DbParameter value) => throw new NotImplementedException(); + } + + internal struct StructQueryParam + { + public int Id { get; set; } + } + + internal struct StructQueryParam2 + { + public int Id { get; set; } + public int Value { get; set; } + } + + /// + /// Properties declared in order B, A — but ctor takes (a, b). + /// This triggers the "ok=false" path in CreateParamInfoGenerator, then + /// the hard-way sort by ctor position. + /// + internal class OutOfOrderCtorParam + { + public int B { get; } + public string A { get; } + public OutOfOrderCtorParam(string a, int b) + { + A = a; + B = b; + } + } + + /// + /// Property C is not in the ctor → hard-way sort fails → falls back to alphabetical sort. + /// + internal class MismatchedCtorParam + { + public int A { get; } + public int C { get; } + public MismatchedCtorParam(int a, int c) + { + A = a; + C = c; + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.SqlMapperMisc.cs b/tests/Dapper.Tests/FakeDbTests.SqlMapperMisc.cs new file mode 100644 index 000000000..5157059ac --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.SqlMapperMisc.cs @@ -0,0 +1,643 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers miscellaneous uncovered paths in SqlMapper.cs: + /// TypeMapEntry methods, GetCachedSQL filter, GetHashCollissions, + /// AddTypeMap/RemoveTypeMap/HasTypeHandler, SetDbType, LookupDbType paths, + /// ExecuteScalar/ExecuteReader/Query overloads, CompiledRegex.PseudoPositional, + /// ThrowMultipleRows/ThrowZeroRows, and multimap overloads. + /// + public class FakeDbSqlMapperMiscTests + { + // ── TypeMapEntry internal struct methods ────────────────────── + + [Fact] + public void TypeMapEntry_GetHashCode_ReturnsValue() + { + var entry = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + var hash = entry.GetHashCode(); + Assert.NotEqual(0, hash); + } + + [Fact] + public void TypeMapEntry_ToString_ContainsDbType() + { + var entry = new SqlMapper.TypeMapEntry(DbType.String, SqlMapper.TypeMapEntryFlags.SetType); + var s = entry.ToString(); + Assert.Contains("String", s); + } + + [Fact] + public void TypeMapEntry_Equals_SameValues_ReturnsTrue() + { + var a = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + var b = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + Assert.True(a.Equals(b)); + } + + [Fact] + public void TypeMapEntry_Equals_Object_ReturnsTrueForSame() + { + var a = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + object b = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + Assert.True(a.Equals(b)); + } + + [Fact] + public void TypeMapEntry_Equals_Object_ReturnsFalseForDifferent() + { + var a = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + object b = new SqlMapper.TypeMapEntry(DbType.String, SqlMapper.TypeMapEntryFlags.SetType); + Assert.False(a.Equals(b)); + } + + [Fact] + public void TypeMapEntry_Equals_Object_ReturnsFalseForNull() + { + var a = new SqlMapper.TypeMapEntry(DbType.Int32, SqlMapper.TypeMapEntryFlags.SetType); + Assert.False(a.Equals(null)); + } + + // ── GetCachedSQL with ignoreHitCountAbove filter ─────────────── + + [Fact] + public void GetCachedSQL_WithFilter_ReturnsFilteredResults() + { + // Run a query to populate cache + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 1 } } }); + conn.Open(); + conn.QueryFirst("SELECT v FROM CacheSQLFilter_Test"); + + // Call with small ignoreHitCountAbove to exercise the Where path (L144) + var results = SqlMapper.GetCachedSQL(999).ToList(); + Assert.NotNull(results); + } + + // ── GetHashCollissions ───────────────────────────────────────── + + [Fact] + public void GetHashCollissions_Returns_Enumerable() + { + // Just enumerate — may be empty, but covers all code paths L152-169 + var collisions = SqlMapper.GetHashCollissions().ToList(); + Assert.NotNull(collisions); + } + + // ── AddTypeMap with useGetFieldValue = true ──────────────────── + + [Fact] + public void AddTypeMap_WithUseGetFieldValue_SetsFlag() + { + // Covers L303-305: the useGetFieldValue=true path + SqlMapper.AddTypeMap(typeof(MiscTestStruct), DbType.String, useGetFieldValue: true); + // Clean up + SqlMapper.RemoveTypeMap(typeof(MiscTestStruct)); + } + + // ── RemoveTypeMap — removes existing type ───────────────────── + + [Fact] + public void RemoveTypeMap_ExistingType_RemovesIt() + { + // Add first, then remove — covers L334-337 + SqlMapper.AddTypeMap(typeof(MiscTestStruct2), DbType.String); + SqlMapper.RemoveTypeMap(typeof(MiscTestStruct2)); + // Removing a type that doesn't exist is a no-op + SqlMapper.RemoveTypeMap(typeof(MiscTestStruct2)); // no-op: covers L332 + } + + // ── HasTypeHandler ───────────────────────────────────────────── + + [Fact] + public void HasTypeHandler_UnknownType_ReturnsFalse() + { + Assert.False(SqlMapper.HasTypeHandler(typeof(MiscTestStruct3))); + } + + [Fact] + public void HasTypeHandler_KnownType_ReturnsTrue() + { + SqlMapper.AddTypeHandler(typeof(MiscTestStruct3), new MiscTypeHandler()); + try + { + Assert.True(SqlMapper.HasTypeHandler(typeof(MiscTestStruct3))); + } + finally + { + // Clean up + SqlMapper.ResetTypeHandlers(); + } + } + + // ── AddTypeHandlerImpl (obsolete error:true) via reflection ─── + + [Fact] + public void AddTypeHandlerImpl_ViaReflection_InvokesCore() + { + // [Obsolete(error:true)] prevents direct call; use reflection + var method = typeof(SqlMapper).GetMethod("AddTypeHandlerImpl", + BindingFlags.Public | BindingFlags.Static)!; + // passing null handler removes the handler + method.Invoke(null, new object?[] { typeof(MiscTestStruct4), null, true }); + } + + // ── SetDbType (obsolete warning:false) ──────────────────────── + + [Fact] + public void SetDbType_WithValue_SetsDbType() + { + var param = new MinimalDbParameter2(); // already defined in TVPParameter test +#pragma warning disable CS0618 + SqlMapper.SetDbType(param, 42); +#pragma warning restore CS0618 + // DbType.Int32 should be set + Assert.Equal(DbType.Int32, param.DbType); + } + + [Fact] + public void SetDbType_WithNull_IsNoOp() + { + var param = new MinimalDbParameter2(); +#pragma warning disable CS0618 + SqlMapper.SetDbType(param, null); +#pragma warning restore CS0618 + // L442: returns early, DbType stays default + Assert.Equal(DbType.Object, param.DbType); + } + + [Fact] + public void SetDbType_WithDBNull_IsNoOp() + { + var param = new MinimalDbParameter2(); +#pragma warning disable CS0618 + SqlMapper.SetDbType(param, DBNull.Value); +#pragma warning restore CS0618 + Assert.Equal(DbType.Object, param.DbType); + } + + // ── LookupDbType: enum type ──────────────────────────────────── + + [Fact] + public void LookupDbType_EnumType_ReturnsUnderlyingDbType() + { +#pragma warning disable CS0618 + var result = SqlMapper.LookupDbType(typeof(DayOfWeek), "day", false, out _); +#pragma warning restore CS0618 + // DayOfWeek → int → DbType.Int32 + Assert.Equal(DbType.Int32, result); + } + + // ── LookupDbType: type mapped with SetType=0 (DoNotSet) ─────── + + [Fact] + public void LookupDbType_DoNotSetType_ReturnsNull() + { + // Add with dbType < 0 so SetType flag is not set + SqlMapper.AddTypeMap(typeof(MiscTestDoNotSet), (DbType)(-2), false); + try + { +#pragma warning disable CS0618 + var result = SqlMapper.LookupDbType(typeof(MiscTestDoNotSet), "x", false, out _); +#pragma warning restore CS0618 + Assert.Null(result); + } + finally + { + SqlMapper.RemoveTypeMap(typeof(MiscTestDoNotSet)); + } + } + + // ── LookupDbType: IEnumerable auto-detect ──────── + + [Fact] + public void LookupDbType_IEnumerableIDataRecord_ReturnsObject() + { +#pragma warning disable CS0618 + var result = SqlMapper.LookupDbType(typeof(IEnumerable), "recs", false, out var handler); +#pragma warning restore CS0618 + Assert.Equal(DbType.Object, result); + Assert.NotNull(handler); + } + + // ── LookupDbType: demand=true with unregistered type ────────── + + [Fact] + public void LookupDbType_DemandTrue_UnknownType_Throws() + { + Assert.Throws(() => + { +#pragma warning disable CS0618 + SqlMapper.LookupDbType(typeof(FakeUnmappedStruct), "field", true, out _); +#pragma warning restore CS0618 + }); + } + + // ── CompiledRegex.PseudoPositional ──────────────────────────── + + [Fact] + public void CompiledRegex_PseudoPositional_Matches() + { + Assert.True(CompiledRegex.PseudoPositional.IsMatch("?param?")); + Assert.False(CompiledRegex.PseudoPositional.IsMatch("@param")); + } + + // ── ExecuteScalar(string) overload ──────────────────────────── + + [Fact] + public void ExecuteScalar_StringOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 42 } } }); + conn.Open(); + var result = conn.ExecuteScalar("SELECT 42"); + Assert.Equal(42, result); + } + + // ── ExecuteScalar(CommandDefinition) overload ───────────────── + + [Fact] + public void ExecuteScalar_CommandDefinitionOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 99 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT_ExecuteScalar_CmdDef_Test"); + SqlMapper.PurgeQueryCache(); + var result = conn.ExecuteScalar(cmd); + Assert.NotNull(result); + } + + // ── ExecuteReader(CommandDefinition) overload ───────────────── + + [Fact] + public void ExecuteReader_CommandDefinitionOverload_ReturnsReader() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT Id FROM T"); + using var reader = conn.ExecuteReader(cmd); + Assert.True(reader.Read()); + } + + // ── ExecuteReader(CommandDefinition, CommandBehavior) overload ── + + [Fact] + public void ExecuteReader_CommandDefinitionAndBehavior_ReturnsReader() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 2 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT Id FROM T"); + using var reader = conn.ExecuteReader(cmd, CommandBehavior.Default); + Assert.True(reader.Read()); + } + + // ── QueryFirstOrDefault dynamic overload (string) ───────────── + + [Fact] + public void QueryFirstOrDefault_Dynamic_StringOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 5 } } }); + conn.Open(); + dynamic? result = conn.QueryFirstOrDefault("SELECT Id FROM T"); + Assert.NotNull(result); + } + + [Fact] + public void QueryFirstOrDefault_Dynamic_StringOverload_NoRows_ReturnsNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + dynamic? result = conn.QueryFirstOrDefault("SELECT Id FROM T"); + Assert.Null(result); + } + + // ── QuerySingle dynamic overload (string) ───────────────────── + + [Fact] + public void QuerySingle_Dynamic_StringOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 9 } } }); + conn.Open(); + dynamic result = conn.QuerySingle("SELECT Id FROM T"); + Assert.NotNull(result); + } + + // ── QueryFirst(Type, string) overload ───────────────────────── + + [Fact] + public void QueryFirst_TypeOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 11 } } }); + conn.Open(); + var result = conn.QueryFirst(typeof(int), "SELECT 11"); + Assert.Equal(11, result); + } + + [Fact] + public void QueryFirst_TypeOverload_NullType_Throws() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + Assert.Throws(() => conn.QueryFirst(null!, "SELECT 1")); + } + + // ── QueryFirstOrDefault(Type, string) overload ──────────────── + + [Fact] + public void QueryFirstOrDefault_TypeOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 12 } } }); + conn.Open(); + var result = conn.QueryFirstOrDefault(typeof(int), "SELECT 12"); + Assert.Equal(12, result); + } + + // ── QuerySingle(Type, string) overload ──────────────────────── + + [Fact] + public void QuerySingle_TypeOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 13 } } }); + conn.Open(); + var result = conn.QuerySingle(typeof(int), "SELECT 13"); + Assert.Equal(13, result); + } + + // ── QuerySingleOrDefault(Type, string) overload ─────────────── + + [Fact] + public void QuerySingleOrDefault_TypeOverload_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 14 } } }); + conn.Open(); + var result = conn.QuerySingleOrDefault(typeof(int), "SELECT 14"); + Assert.Equal(14, result); + } + + // ── QueryFirst(CommandDefinition) overload ───────────────── + + [Fact] + public void QueryFirst_CommandDefinition_Generic_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 15 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT 15"); + var result = conn.QueryFirst(cmd); + Assert.Equal(15, result); + } + + // ── QueryFirstOrDefault(CommandDefinition) overload ──────── + + [Fact] + public void QueryFirstOrDefault_CommandDefinition_Generic_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 16 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT 16"); + var result = conn.QueryFirstOrDefault(cmd); + Assert.Equal(16, result); + } + + // ── QuerySingle(CommandDefinition) overload ──────────────── + + [Fact] + public void QuerySingle_CommandDefinition_Generic_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 17 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT 17"); + var result = conn.QuerySingle(cmd); + Assert.Equal(17, result); + } + + // ── QuerySingleOrDefault(CommandDefinition) overload ─────── + + [Fact] + public void QuerySingleOrDefault_CommandDefinition_Generic_ReturnsValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 18 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT 18"); + var result = conn.QuerySingleOrDefault(cmd); + Assert.Equal(18, result); + } + + // ── QueryMultiple(CommandDefinition) overload ───────────────── + + [Fact] + public void QueryMultiple_CommandDefinition_Overload_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "v", 19 } } }); + conn.Open(); + var cmd = new CommandDefinition("SELECT 19"); + using var grid = conn.QueryMultiple(cmd); + var result = grid.Read().First(); + Assert.Equal(19, result); + } + + // ── ThrowMultipleRows: Row.Single ───────────────────────────── + + [Fact] + public void QuerySingle_MultipleRows_ThrowsInvalidOperation() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "v", 1 } }, + new Dictionary { { "v", 2 } } + }); + conn.Open(); + Assert.Throws(() => conn.QuerySingle("SELECT v FROM T")); + } + + // ── ThrowMultipleRows: Row.SingleOrDefault ──────────────────── + + [Fact] + public void QuerySingleOrDefault_MultipleRows_ThrowsInvalidOperation() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "v", 1 } }, + new Dictionary { { "v", 2 } } + }); + conn.Open(); + Assert.Throws(() => conn.QuerySingleOrDefault("SELECT v FROM T")); + } + + // ── ThrowZeroRows: Row.Single (0 rows) ──────────────────────── + + [Fact] + public void QuerySingle_ZeroRows_ThrowsInvalidOperation() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + Assert.Throws(() => conn.QuerySingle("SELECT v FROM T")); + } + + // ── ThrowZeroRows via QueryFirst (Row.First) ───────────────── + + [Fact] + public void QueryFirst_ZeroRows_ThrowsInvalidOperation() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(Array.Empty>()); + conn.Open(); + Assert.Throws(() => conn.QueryFirst("SELECT v FROM T")); + } + + // ── 4-type MultiMap overload ────────────────────────────────── + + [Fact] + public void Query_4TypeMultiMap_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary + { + { "AId", 1 }, { "BId", 2 }, { "CId", 3 }, { "DId", 4 } + } + }); + conn.Open(); + + var results = conn.Query( + "SELECT AId, BId, CId, DId FROM T", + (a, b, c, d) => $"{a.AId},{b.BId},{c.CId},{d.DId}", + splitOn: "BId,CId,DId").ToList(); + + Assert.Single(results); + } + + // ── 5-type MultiMap overload ────────────────────────────────── + + [Fact] + public void Query_5TypeMultiMap_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary + { + { "AId", 1 }, { "BId", 2 }, { "CId", 3 }, { "DId", 4 }, { "EId", 5 } + } + }); + conn.Open(); + + var results = conn.Query( + "SELECT AId, BId, CId, DId, EId FROM T", + (a, b, c, d, e) => $"{a.AId},{b.BId},{c.CId},{d.DId},{e.EId}", + splitOn: "BId,CId,DId,EId").ToList(); + + Assert.Single(results); + } + + // ── 6-type MultiMap overload ────────────────────────────────── + + [Fact] + public void Query_6TypeMultiMap_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary + { + { "AId", 1 }, { "BId", 2 }, { "CId", 3 }, { "DId", 4 }, { "EId", 5 }, { "FId", 6 } + } + }); + conn.Open(); + + var results = conn.Query( + "SELECT AId, BId, CId, DId, EId, FId FROM T", + (a, b, c, d, e, f) => $"{a.AId},{b.BId},{c.CId},{d.DId},{e.EId},{f.FId}", + splitOn: "BId,CId,DId,EId,FId").ToList(); + + Assert.Single(results); + } + + // ── 7-type MultiMap overload ────────────────────────────────── + + [Fact] + public void Query_7TypeMultiMap_ReturnsResults() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary + { + { "AId", 1 }, { "BId", 2 }, { "CId", 3 }, { "DId", 4 }, { "EId", 5 }, { "FId", 6 }, { "GId", 7 } + } + }); + conn.Open(); + + var results = conn.Query( + "SELECT AId, BId, CId, DId, EId, FId, GId FROM T", + (a, b, c, d, e, f, g) => $"{a.AId},{b.BId},{c.CId},{d.DId},{e.EId},{f.FId},{g.GId}", + splitOn: "BId,CId,DId,EId,FId,GId").ToList(); + + Assert.Single(results); + } + + // ── MultiMapImpl with empty types array ─────────────────────── + + [Fact] + public void Query_TypeArray_EmptyTypes_ThrowsArgumentException() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + Assert.Throws(() => + conn.Query("SELECT 1", Array.Empty(), objects => "x").ToList()); + } + } + + // ── Helper types ────────────────────────────────────────────────── + + internal struct MiscTestStruct { } + internal struct MiscTestStruct2 { } + internal struct MiscTestStruct3 { } + internal struct MiscTestStruct4 { } + internal struct MiscTestDoNotSet { } + internal struct FakeUnmappedStruct { } + + internal class MiscTypeHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, MiscTestStruct3 value) + => parameter.Value = DBNull.Value; + public override MiscTestStruct3 Parse(object value) => default; + } + + internal class MiscA { public int AId { get; set; } } + internal class MiscB { public int BId { get; set; } } + internal class MiscC { public int CId { get; set; } } + internal class MiscD { public int DId { get; set; } } + internal class MiscE { public int EId { get; set; } } + internal class MiscF { public int FId { get; set; } } + internal class MiscG { public int GId { get; set; } } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.SqlMapperSettings.cs b/tests/Dapper.Tests/FakeDbTests.SqlMapperSettings.cs new file mode 100644 index 000000000..470ec0b0f --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.SqlMapperSettings.cs @@ -0,0 +1,230 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Additional tests for SqlMapper.Settings properties not yet covered, + /// and DefaultTypeMap features. + /// + public class FakeDbSqlMapperSettingsTests + { + // ── SqlMapper.Settings — additional properties ───────────────── + + [Fact] + public void Settings_UseSingleResultOptimization_CanBeSet() + { + var original = SqlMapper.Settings.UseSingleResultOptimization; + try + { + SqlMapper.Settings.UseSingleResultOptimization = false; + Assert.False(SqlMapper.Settings.UseSingleResultOptimization); + SqlMapper.Settings.UseSingleResultOptimization = true; + Assert.True(SqlMapper.Settings.UseSingleResultOptimization); + } + finally + { + SqlMapper.Settings.UseSingleResultOptimization = original; + } + } + + [Fact] + public void Settings_UseSingleRowOptimization_CanBeSet() + { + var original = SqlMapper.Settings.UseSingleRowOptimization; + try + { + SqlMapper.Settings.UseSingleRowOptimization = false; + Assert.False(SqlMapper.Settings.UseSingleRowOptimization); + SqlMapper.Settings.UseSingleRowOptimization = true; + Assert.True(SqlMapper.Settings.UseSingleRowOptimization); + } + finally + { + SqlMapper.Settings.UseSingleRowOptimization = original; + } + } + + [Fact] + public void Settings_InListStringSplitCount_CanBeSet() + { + var original = SqlMapper.Settings.InListStringSplitCount; + try + { + SqlMapper.Settings.InListStringSplitCount = 5; + Assert.Equal(5, SqlMapper.Settings.InListStringSplitCount); + } + finally + { + SqlMapper.Settings.InListStringSplitCount = original; + } + } + + [Fact] + public void Settings_FetchSize_CanBeSet() + { + var original = SqlMapper.Settings.FetchSize; + try + { + SqlMapper.Settings.FetchSize = 1024; + Assert.Equal(1024, SqlMapper.Settings.FetchSize); + } + finally + { + SqlMapper.Settings.FetchSize = original; + } + } + + [Fact] + public void Settings_SupportLegacyParameterTokens_CanBeSet() + { + var original = SqlMapper.Settings.SupportLegacyParameterTokens; + try + { + SqlMapper.Settings.SupportLegacyParameterTokens = false; + Assert.False(SqlMapper.Settings.SupportLegacyParameterTokens); + } + finally + { + SqlMapper.Settings.SupportLegacyParameterTokens = original; + } + } + + [Fact] + public void Settings_SetDefaults_Works() + { + // SetDefaults resets all settings to defaults — just verify it doesn't throw + SqlMapper.Settings.SetDefaults(); + // Verify defaults were restored + Assert.Null(SqlMapper.Settings.CommandTimeout); + } + + // ── DefaultTypeMap — additional operations ──────────────────── + + private class Underscore + { + public int UserId { get; set; } + public string? FirstName { get; set; } + } + + [Fact] + public void DefaultTypeMap_MatchNamesWithUnderscores_Works() + { + var original = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "user_id", 1 }, + { "first_name", "Alice" } + } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT user_id, first_name FROM T"); + Assert.Equal(1, result.UserId); + Assert.Equal("Alice", result.FirstName); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = original; + } + } + + [Fact] + public void DefaultTypeMap_FindConstructor_DefaultCtor_Works() + { + var map = new DefaultTypeMap(typeof(Underscore)); + // Empty param list matches the default constructor + var ctor = map.FindConstructor(System.Array.Empty(), System.Array.Empty()); + Assert.NotNull(ctor); + } + + [Fact] + public void DefaultTypeMap_FindExplicitConstructor_NoAttribute_ReturnsNull() + { + var map = new DefaultTypeMap(typeof(Underscore)); + var ctor = map.FindExplicitConstructor(); + + // No [ExplicitConstructor] attribute on Underscore + Assert.Null(ctor); + } + + [Fact] + public void DefaultTypeMap_GetConstructorParameter_Works() + { + // A type with a single-arg constructor + var map = new DefaultTypeMap(typeof(SingleCtorClass)); + var ctor = map.FindConstructor(new[] { "id" }, new[] { typeof(int) }); + if (ctor is not null) + { + var member = map.GetConstructorParameter(ctor, "id"); + Assert.NotNull(member); + } + } + + [Fact] + public void DefaultTypeMap_GetMember_FieldMapping_Works() + { + // DefaultTypeMap also maps public fields + var map = new DefaultTypeMap(typeof(FieldClass)); + var member = map.GetMember("Value"); + Assert.NotNull(member); + } + + [Fact] + public void DefaultTypeMap_Properties_IsNotEmpty() + { + var map = new DefaultTypeMap(typeof(Underscore)); + Assert.NotEmpty(map.Properties); + } + + // ── Helper types ────────────────────────────────────────────── + + private class SingleCtorClass + { + public int Id { get; } + public SingleCtorClass(int id) { Id = id; } + } + + private class FieldClass + { + public int Value; + } + + // ── AsList extension ────────────────────────────────────────── + + [Fact] + public void AsList_FromList_ReturnsSameList() + { + var list = new System.Collections.Generic.List { 1, 2, 3 }; + var result = list.AsList(); + Assert.Same(list, result); + } + + [Fact] + public void AsList_FromArray_ReturnsNewList() + { + int[] array = { 1, 2, 3 }; + var result = array.AsList(); + Assert.Equal(3, result.Count); + } + + [Fact] + public void AsList_FromNull_ReturnsNull() + { + System.Collections.Generic.IEnumerable? nullSeq = null; + var result = nullSeq.AsList(); + // AsList returns null! when source is null + Assert.Null(result); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.TVPParameter.cs b/tests/Dapper.Tests/FakeDbTests.TVPParameter.cs new file mode 100644 index 000000000..a394b0acb --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.TVPParameter.cs @@ -0,0 +1,221 @@ +#if !NET481 +using System.Collections.Generic; +using System.Data; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers SqlDataRecordListTVPParameter<T> (lines 14-43) and StructuredHelper (lines 44-105): + /// AddParameter, Set (null/empty/non-empty), ConfigureTVP, ConfigureUDT, IL generation, cache. + /// + public class FakeDbTVPParameterTests + { + // ── AddParameter via DynamicParameters.Add ──────────────────── + // SqlDataRecordListTVPParameter.AddParameter creates param, calls Set, adds to command. + + [Fact] + public void TVPParameter_AddParameter_NonEmpty_AddsParamWithValue() + { + var records = new List { new SimpleDataRecord2() }; + var tvp = new SqlDataRecordListTVPParameter(records, "dbo.MyType"); + + var dp = new DynamicParameters(); + dp.Add("ids", tvp); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("EXEC sp @ids", dp); + } + + [Fact] + public void TVPParameter_AddParameter_EmptyList_AddsParamWithNull() + { + var records = new List(); + var tvp = new SqlDataRecordListTVPParameter(records, "dbo.MyType"); + + var dp = new DynamicParameters(); + dp.Add("ids", tvp); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(0); + conn.Open(); + + conn.Execute("EXEC sp @ids", dp); + } + + // ── Set() static — null data ────────────────────────────────── + + [Fact] + public void TVPParameter_Set_NullData_SetsNullValue() + { + var param = new MinimalDbParameter2(); + SqlDataRecordListTVPParameter.Set(param, null, null); + Assert.Null(param.Value); + } + + // ── Set() static — empty data ───────────────────────────────── + + [Fact] + public void TVPParameter_Set_EmptyData_SetsNullValue() + { + var param = new MinimalDbParameter2(); + var empty = new List(); + SqlDataRecordListTVPParameter.Set(param, empty, null); + Assert.Null(param.Value); + } + + // ── Set() static — non-empty data ──────────────────────────── + + [Fact] + public void TVPParameter_Set_NonEmptyData_SetsValue() + { + var param = new MinimalDbParameter2(); + var records = new List { new SimpleDataRecord2() }; + SqlDataRecordListTVPParameter.Set(param, records, null); + Assert.NotNull(param.Value); + } + + // ── StructuredHelper.ConfigureTVP — no TypeName property → no-op ── + + [Fact] + public void StructuredHelper_ConfigureTVP_NoProperty_IsNoOp() + { + var param = new MinimalDbParameter2(); + StructuredHelper.ConfigureTVP(param, "dbo.Type"); + // no TypeName property → no-op; param.Value unchanged + Assert.Null(param.Value); + } + + // ── StructuredHelper.ConfigureTVP — with TypeName property → IL path ── + + [Fact] + public void StructuredHelper_ConfigureTVP_WithTypeNameProperty_SetsTypeName() + { + var param = new FakeParamWithTypeName(); + StructuredHelper.ConfigureTVP(param, "dbo.IdList"); + Assert.Equal("dbo.IdList", param.TypeName); + Assert.Equal(30, param.SqlDbType); // SqlDbType.Structured = 30 + } + + // ── StructuredHelper.ConfigureTVP — cache hit (second call same type) ── + + [Fact] + public void StructuredHelper_ConfigureTVP_SecondCall_UsesCachedDelegate() + { + var p1 = new FakeParamWithTypeName(); + var p2 = new FakeParamWithTypeName(); + + StructuredHelper.ConfigureTVP(p1, "dbo.Type1"); + StructuredHelper.ConfigureTVP(p2, "dbo.Type2"); + + Assert.Equal("dbo.Type1", p1.TypeName); + Assert.Equal("dbo.Type2", p2.TypeName); + } + + // ── StructuredHelper.ConfigureUDT — with UdtTypeName property → IL path ── + // (ConfigureUDT is also tested via UdtTypeHandler, but test coverage for StructuredHelper here) + + [Fact] + public void StructuredHelper_ConfigureUDT_WithUdtTypeNameProperty_SetsTypeName() + { + var param = new FakeParamWithUdtOnly(); + StructuredHelper.ConfigureUDT(param, "dbo.Point"); + Assert.Equal("dbo.Point", param.UdtTypeName); + Assert.Equal(29, param.SqlDbType); // SqlDbType.Udt = 29 + } + + // ── StructuredHelper.ConfigureTVP — TypeName property not writable → no-op ── + + [Fact] + public void StructuredHelper_ConfigureTVP_ReadOnlyProperty_IsNoOp() + { + var param = new FakeParamWithReadOnlyTypeName(); + StructuredHelper.ConfigureTVP(param, "dbo.Table"); + // no setter → no-op; TypeName stays at default + Assert.Equal("readonly-default", param.TypeName); + } + + // ── SqlDbType property present but not writable → skipped ───── + + [Fact] + public void StructuredHelper_ConfigureTVP_WithTypeNameOnly_NoSqlDbType_Works() + { + var param = new FakeParamWithTypeNameNoSqlDbType(); + StructuredHelper.ConfigureTVP(param, "dbo.Tbl"); + Assert.Equal("dbo.Tbl", param.TypeName); + } + } + + // ── Helper parameter types ───────────────────────────────────────── + + internal class MinimalDbParameter2 : IDbDataParameter + { + public DbType DbType { get; set; } = DbType.Object; + public ParameterDirection Direction { get; set; } = ParameterDirection.Input; + public bool IsNullable => false; + public string? ParameterName { get; set; } + public string? SourceColumn { get; set; } + public DataRowVersion SourceVersion { get; set; } = DataRowVersion.Default; + public object? Value { get; set; } + public byte Precision { get; set; } + public byte Scale { get; set; } + public int Size { get; set; } + } + + internal class FakeParamWithTypeName : MinimalDbParameter2 + { + public string? TypeName { get; set; } + public int SqlDbType { get; set; } + } + + internal class FakeParamWithUdtOnly : MinimalDbParameter2 + { + public string? UdtTypeName { get; set; } + public int SqlDbType { get; set; } + } + + internal class FakeParamWithReadOnlyTypeName : MinimalDbParameter2 + { + public string TypeName => "readonly-default"; // no setter + } + + internal class FakeParamWithTypeNameNoSqlDbType : MinimalDbParameter2 + { + public string? TypeName { get; set; } + // no SqlDbType property + } + + internal class SimpleDataRecord2 : IDataRecord + { + public int FieldCount => 0; + public object this[int i] => throw new System.NotImplementedException(); + public object this[string name] => throw new System.NotImplementedException(); + public bool GetBoolean(int i) => throw new System.NotImplementedException(); + public byte GetByte(int i) => throw new System.NotImplementedException(); + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new System.NotImplementedException(); + public char GetChar(int i) => throw new System.NotImplementedException(); + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new System.NotImplementedException(); + public IDataReader GetData(int i) => throw new System.NotImplementedException(); + public string GetDataTypeName(int i) => throw new System.NotImplementedException(); + public System.DateTime GetDateTime(int i) => throw new System.NotImplementedException(); + public decimal GetDecimal(int i) => throw new System.NotImplementedException(); + public double GetDouble(int i) => throw new System.NotImplementedException(); + public System.Type GetFieldType(int i) => throw new System.NotImplementedException(); + public float GetFloat(int i) => throw new System.NotImplementedException(); + public System.Guid GetGuid(int i) => throw new System.NotImplementedException(); + public short GetInt16(int i) => throw new System.NotImplementedException(); + public int GetInt32(int i) => throw new System.NotImplementedException(); + public long GetInt64(int i) => throw new System.NotImplementedException(); + public string GetName(int i) => throw new System.NotImplementedException(); + public int GetOrdinal(string name) => throw new System.NotImplementedException(); + public string GetString(int i) => throw new System.NotImplementedException(); + public object GetValue(int i) => throw new System.NotImplementedException(); + public int GetValues(object[] values) => throw new System.NotImplementedException(); + public bool IsDBNull(int i) => throw new System.NotImplementedException(); + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.TypeHandlers.cs b/tests/Dapper.Tests/FakeDbTests.TypeHandlers.cs new file mode 100644 index 000000000..470222f56 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.TypeHandlers.cs @@ -0,0 +1,90 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbTypeHandlerTests + { + private class GuidStringHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, Guid value) + => parameter.Value = value.ToString("D"); + + public override Guid Parse(object value) + => Guid.Parse(value.ToString()!); + } + + [Fact] + public void TypeHandler_Parse_IsInvokedWhenReadingColumn() + { + SqlMapper.AddTypeHandler(new GuidStringHandler()); + try + { + var id = Guid.NewGuid(); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "GuidId", id.ToString("D") } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT GuidId FROM Items"); + + Assert.Equal(id, result.GuidId); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + private class GuidRow + { + public Guid GuidId { get; set; } + } + + [Fact] + public void TypeHandler_SetValue_IsInvokedWhenPassingParameter() + { + var handler = new TrackingHandler(); + SqlMapper.AddTypeHandler(handler); + try + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("INSERT INTO Items (Val) VALUES (@val)", + new { val = new TrackedType { Value = "x" } }); + + Assert.True(handler.SetValueCalled); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + private class TrackedType { public string? Value { get; set; } } + + private class TrackingHandler : SqlMapper.TypeHandler + { + public bool SetValueCalled { get; private set; } + + public override void SetValue(IDbDataParameter parameter, TrackedType value) + { + SetValueCalled = true; + parameter.Value = value.Value; + } + + public override TrackedType Parse(object value) + => new TrackedType { Value = value?.ToString() }; + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.TypeMapping.cs b/tests/Dapper.Tests/FakeDbTests.TypeMapping.cs new file mode 100644 index 000000000..fea5d5aec --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.TypeMapping.cs @@ -0,0 +1,295 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbTypeMappingTests + { + // ── AddTypeMap / GetTypeMap ──────────────────────────────────── + + [Fact] + public void AddTypeMap_RegistersCustomMapping() + { + // Add a custom mapping and verify it can be retrieved + SqlMapper.AddTypeMap(typeof(DateOnly), DbType.Date); + // No exception = success; the type map is updated + } + + [Fact] + public void GetTypeMap_ReturnsDefaultTypeMap_ForUnmappedType() + { + var map = SqlMapper.GetTypeMap(typeof(User)); + Assert.NotNull(map); + } + + // ── SetTypeMap / CustomPropertyTypeMap ───────────────────────── + + private class Odd { public string? first_name { get; set; } public string? last_name { get; set; } } + + [Fact] + public void SetTypeMap_CustomPropertySelector_RemapsColumns() + { + SqlMapper.SetTypeMap(typeof(Odd), + new CustomPropertyTypeMap(typeof(Odd), (type, col) => col switch + { + "fn" => type.GetProperty(nameof(Odd.first_name))!, + "ln" => type.GetProperty(nameof(Odd.last_name))!, + _ => null! + })); + try + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "fn", "John" }, { "ln", "Doe" } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT fn, ln FROM People"); + Assert.Equal("John", result.first_name); + Assert.Equal("Doe", result.last_name); + } + finally + { + SqlMapper.SetTypeMap(typeof(Odd), null); // reset + } + } + + [Fact] + public void CustomPropertyTypeMap_FindConstructor_ReturnsDefault() + { + var map = new CustomPropertyTypeMap(typeof(User), + (t, col) => t.GetProperty(col, BindingFlags.Public | BindingFlags.Instance)!); + + var ctor = map.FindConstructor(Array.Empty(), Array.Empty()); + Assert.NotNull(ctor); + } + + [Fact] + public void CustomPropertyTypeMap_FindExplicitConstructor_ReturnsNull() + { + var map = new CustomPropertyTypeMap(typeof(User), + (t, col) => t.GetProperty(col, BindingFlags.Public | BindingFlags.Instance)!); + + Assert.Null(map.FindExplicitConstructor()); + } + + [Fact] + public void CustomPropertyTypeMap_GetMember_ReturnsNull_WhenSelectorReturnsNull() + { + var map = new CustomPropertyTypeMap(typeof(User), (t, col) => null!); + Assert.Null(map.GetMember("AnyColumn")); + } + + [Fact] + public void CustomPropertyTypeMap_GetConstructorParameter_ThrowsNotSupported() + { + var map = new CustomPropertyTypeMap(typeof(User), (t, col) => null!); + var ctor = typeof(User).GetConstructor(Type.EmptyTypes)!; + Assert.Throws(() => + map.GetConstructorParameter(ctor, "Id")); + } + + [Fact] + public void CustomPropertyTypeMap_Constructor_ThrowsOnNullType() + { + Assert.Throws(() => + new CustomPropertyTypeMap(null!, (t, col) => null!)); + } + + [Fact] + public void CustomPropertyTypeMap_Constructor_ThrowsOnNullSelector() + { + Assert.Throws(() => + new CustomPropertyTypeMap(typeof(User), null!)); + } + + // ── XML type handlers ───────────────────────────────────────── + + [Fact] + public void XmlDocument_TypeHandler_ParsesXml() + { + SqlMapper.AddTypeHandler(new XmlDocumentHandler()); + try + { + const string xml = "1"; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "XmlCol", xml } } }); + conn.Open(); + + var result = conn.QueryFirst("SELECT XmlCol FROM T"); + Assert.NotNull(result.XmlCol); + Assert.Equal("root", result.XmlCol!.DocumentElement!.Name); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + [Fact] + public void XDocument_TypeHandler_ParsesXml() + { + SqlMapper.AddTypeHandler(new XDocumentHandler()); + try + { + const string xml = "hello"; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "XmlCol", xml } } }); + conn.Open(); + + var result = conn.QueryFirst("SELECT XmlCol FROM T"); + Assert.NotNull(result.XmlCol); + Assert.Equal("root", result.XmlCol!.Root!.Name.LocalName); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + [Fact] + public void XElement_TypeHandler_ParsesXml() + { + SqlMapper.AddTypeHandler(new XElementHandler()); + try + { + const string xml = "text"; + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "XmlCol", xml } } }); + conn.Open(); + + var result = conn.QueryFirst("SELECT XmlCol FROM T"); + Assert.NotNull(result.XmlCol); + Assert.Equal("item", result.XmlCol!.Name.LocalName); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + [Fact] + public void XmlDocument_TypeHandler_SetsDbTypeXml_OnParameter() + { + SqlMapper.AddTypeHandler(new XmlDocumentHandler()); + try + { + var doc = new XmlDocument(); + doc.LoadXml(""); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("INSERT INTO T (XmlCol) VALUES (@xml)", new { xml = doc }); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + [Fact] + public void XDocument_TypeHandler_SetsDbTypeXml_OnParameter() + { + SqlMapper.AddTypeHandler(new XDocumentHandler()); + try + { + var xdoc = XDocument.Parse(""); + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(1); + conn.Open(); + + conn.Execute("INSERT INTO T (XmlCol) VALUES (@xml)", new { xml = xdoc }); + } + finally + { + SqlMapper.ResetTypeHandlers(); + } + } + + // ── FeatureSupport ──────────────────────────────────────────── + + [Fact] + public void FeatureSupport_NullConnection_ReturnsDefault() + { + var fs = FeatureSupport.Get(null); + Assert.NotNull(fs); + } + + [Fact] + public void FeatureSupport_FakeConnection_ReturnsDefault() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + var fs = FeatureSupport.Get(conn); + Assert.NotNull(fs); + } + + // ── constructor mapping ─────────────────────────────────────── + + private class ImmutablePoint + { + public int X { get; } + public int Y { get; } + public ImmutablePoint(int x, int y) { X = x; Y = y; } + } + + [Fact] + public void Query_MapsToConstructor() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "X", 3 }, { "Y", 4 } } + }); + conn.Open(); + + var result = conn.QueryFirst("SELECT 3 AS X, 4 AS Y"); + Assert.Equal(3, result.X); + Assert.Equal(4, result.Y); + } + + // ── DefaultTypeMap ──────────────────────────────────────────── + + [Fact] + public void DefaultTypeMap_GetMember_ReturnsMapping_ForExistingProperty() + { + var map = new DefaultTypeMap(typeof(User)); + var member = map.GetMember("Id"); + Assert.NotNull(member); + } + + [Fact] + public void DefaultTypeMap_GetMember_ReturnsNull_ForUnknownColumn() + { + var map = new DefaultTypeMap(typeof(User)); + var member = map.GetMember("DoesNotExist"); + Assert.Null(member); + } + + [Fact] + public void DefaultTypeMap_FindConstructor_ReturnsDefault() + { + var map = new DefaultTypeMap(typeof(User)); + var ctor = map.FindConstructor(Array.Empty(), Array.Empty()); + Assert.NotNull(ctor); + } + + // ── helper POCOs for XML handler tests ──────────────────────── + + private class XmlDocumentRow { public XmlDocument? XmlCol { get; set; } } + private class XDocumentRow { public XDocument? XmlCol { get; set; } } + private class XElementRow { public XElement? XmlCol { get; set; } } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.Types.cs b/tests/Dapper.Tests/FakeDbTests.Types.cs new file mode 100644 index 000000000..a5f623ddb --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Types.cs @@ -0,0 +1,386 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests type coercion paths inside Dapper's mapping engine. + /// Exercising a wide variety of CLR types ensures the type-switch + /// branches in SqlMapper are hit. + /// + public class FakeDbTypeCoercionTests + { + // ── primitive numeric types ───────────────────────────────────── + + [Fact] + public void Query_MapsInt32Column() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 42 } } }); + conn.Open(); + Assert.Equal(42, conn.QueryFirst("SELECT 42")); + } + + [Fact] + public void Query_MapsInt64Column() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 9_000_000_000L } } }); + conn.Open(); + Assert.Equal(9_000_000_000L, conn.QueryFirst("SELECT 9000000000")); + } + + [Fact] + public void Query_MapsInt16Column() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", (short)32767 } } }); + conn.Open(); + Assert.Equal((short)32767, conn.QueryFirst("SELECT 32767")); + } + + [Fact] + public void Query_MapsByteColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", (byte)255 } } }); + conn.Open(); + Assert.Equal((byte)255, conn.QueryFirst("SELECT 255")); + } + + [Fact] + public void Query_MapsBoolColumn_True() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", true } } }); + conn.Open(); + Assert.True(conn.QueryFirst("SELECT 1")); + } + + [Fact] + public void Query_MapsBoolColumn_False() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", false } } }); + conn.Open(); + Assert.False(conn.QueryFirst("SELECT 0")); + } + + [Fact] + public void Query_MapsFloatColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 3.14f } } }); + conn.Open(); + Assert.Equal(3.14f, conn.QueryFirst("SELECT 3.14"), 4); + } + + [Fact] + public void Query_MapsDoubleColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 2.718281828 } } }); + conn.Open(); + Assert.Equal(2.718281828, conn.QueryFirst("SELECT 2.718"), 6); + } + + [Fact] + public void Query_MapsDecimalColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 99.99m } } }); + conn.Open(); + Assert.Equal(99.99m, conn.QueryFirst("SELECT 99.99")); + } + + // ── string / char ─────────────────────────────────────────────── + + [Fact] + public void Query_MapsStringColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", "hello" } } }); + conn.Open(); + Assert.Equal("hello", conn.QueryFirst("SELECT 'hello'")); + } + + [Fact] + public void Query_MapsCharColumn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 'A' } } }); + conn.Open(); + Assert.Equal('A', conn.QueryFirst("SELECT 'A'")); + } + + // ── date / time ───────────────────────────────────────────────── + + [Fact] + public void Query_MapsDateTimeColumn() + { + var dt = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", dt } } }); + conn.Open(); + Assert.Equal(dt, conn.QueryFirst("SELECT GETDATE()")); + } + + [Fact] + public void Query_MapsDateTimeOffsetColumn() + { + var dto = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", dto } } }); + conn.Open(); + Assert.Equal(dto, conn.QueryFirst("SELECT SYSDATETIMEOFFSET()")); + } + + [Fact] + public void Query_MapsTimeSpanColumn() + { + var ts = TimeSpan.FromHours(2.5); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", ts } } }); + conn.Open(); + Assert.Equal(ts, conn.QueryFirst("SELECT '02:30:00'")); + } + + // ── Guid ──────────────────────────────────────────────────────── + + [Fact] + public void Query_MapsGuidColumn() + { + var g = Guid.NewGuid(); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", g } } }); + conn.Open(); + Assert.Equal(g, conn.QueryFirst("SELECT NEWID()")); + } + + // ── nullable types ────────────────────────────────────────────── + + [Fact] + public void Query_MapsNullableInt_WithValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 7 } } }); + conn.Open(); + Assert.Equal(7, conn.QueryFirst("SELECT 7")); + } + + [Fact] + public void Query_MapsNullableInt_WithNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", DBNull.Value } } }); + conn.Open(); + Assert.Null(conn.QueryFirst("SELECT NULL")); + } + + [Fact] + public void Query_MapsNullableBool_WithNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", DBNull.Value } } }); + conn.Open(); + Assert.Null(conn.QueryFirst("SELECT NULL")); + } + + [Fact] + public void Query_MapsNullableDateTime_WithValue() + { + var dt = new DateTime(2025, 1, 1); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", dt } } }); + conn.Open(); + Assert.Equal(dt, conn.QueryFirst("SELECT GETDATE()")); + } + + // ── multi-column POCO ──────────────────────────────────────────── + + private class AllTypesRow + { + public int IntCol { get; set; } + public long LongCol { get; set; } + public bool BoolCol { get; set; } + public double DoubleCol { get; set; } + public decimal DecimalCol { get; set; } + public string? StringCol { get; set; } + public DateTime DateCol { get; set; } + public Guid GuidCol { get; set; } + } + + [Fact] + public void Query_MapsAllColumnTypesToPoco() + { + var g = Guid.NewGuid(); + var dt = new DateTime(2024, 1, 1); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary + { + { "IntCol", 1 }, + { "LongCol", 2L }, + { "BoolCol", true }, + { "DoubleCol", 3.14 }, + { "DecimalCol", 99.99m }, + { "StringCol", "test" }, + { "DateCol", dt }, + { "GuidCol", g }, + } + }); + conn.Open(); + + var row = conn.QueryFirst("SELECT ..."); + + Assert.Equal(1, row.IntCol); + Assert.Equal(2L, row.LongCol); + Assert.True(row.BoolCol); + Assert.Equal(3.14, row.DoubleCol, 10); + Assert.Equal(99.99m, row.DecimalCol); + Assert.Equal("test", row.StringCol); + Assert.Equal(dt, row.DateCol); + Assert.Equal(g, row.GuidCol); + } + + // ── enum mapping ───────────────────────────────────────────────── + + private enum Status { Active = 1, Inactive = 2 } + + private class StatusRow { public Status Status { get; set; } } + + [Fact] + public void Query_MapsIntColumnToEnum() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Status", 1 } } }); + conn.Open(); + + var result = conn.QueryFirst("SELECT 1 AS Status"); + Assert.Equal(Status.Active, result.Status); + } + + [Fact] + public void Query_MapsEnumDirectly() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Val", 2 } } }); + conn.Open(); + + var result = conn.QueryFirst("SELECT 2"); + Assert.Equal(Status.Inactive, result); + } + + // ── value type as scalar ───────────────────────────────────────── + + [Fact] + public void ExecuteScalar_MapsToInt() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(10); + conn.Open(); + Assert.Equal(10, conn.ExecuteScalar("SELECT 10")); + } + + [Fact] + public void ExecuteScalar_MapsToGuid() + { + var g = Guid.NewGuid(); + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(g); + conn.Open(); + Assert.Equal(g, conn.ExecuteScalar("SELECT NEWID()")); + } + + [Fact] + public void ExecuteScalar_MapsToNullableInt_Null() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(null); + conn.Open(); + Assert.Null(conn.ExecuteScalar("SELECT NULL")); + } + + // ── unbuffered query ────────────────────────────────────────────── + + [Fact] + public void Query_Unbuffered_ReturnsLazySequence() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "A" } }, + new Dictionary { { "Id", 2 }, { "Name", "B" } }, + }); + conn.Open(); + + var result = conn.Query("SELECT Id, Name FROM Users", buffered: false).ToList(); + Assert.Equal(2, result.Count); + } + + // ── IEnumerable parameter expansion ────────────────────────────── + + [Fact] + public void Query_WithListParameter_ExpandsToIn() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 3 }, { "Name", "Carol" } }, + }); + conn.Open(); + + var ids = new[] { 1, 3 }; + var result = conn.Query("SELECT Id, Name FROM Users WHERE Id IN @ids", new { ids }) + .ToList(); + + Assert.Equal(2, result.Count); + } + + // ── CommandDefinition overload ──────────────────────────────────── + + [Fact] + public void Query_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 5 }, { "Name", "CD" } } }); + conn.Open(); + + var cmd = new CommandDefinition("SELECT Id, Name FROM Users WHERE Id = @id", + parameters: new { id = 5 }); + var result = conn.Query(cmd).ToList(); + + Assert.Single(result); + Assert.Equal(5, result[0].Id); + } + + [Fact] + public void ExecuteScalar_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueScalarResult(7); + conn.Open(); + + var cmd = new CommandDefinition("SELECT COUNT(*) FROM Users"); + Assert.Equal(7, conn.ExecuteScalar(cmd)); + } + + [Fact] + public void Execute_ViaCommandDefinition_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueNonQueryResult(2); + conn.Open(); + + var cmd = new CommandDefinition("DELETE FROM Users WHERE Active = 0"); + Assert.Equal(2, conn.Execute(cmd)); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.UdtTypeHandler.cs b/tests/Dapper.Tests/FakeDbTests.UdtTypeHandler.cs new file mode 100644 index 000000000..6cdebc7f1 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.UdtTypeHandler.cs @@ -0,0 +1,204 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers UdtTypeHandler (lines 12-37) and SqlDataRecordHandler (lines 10-18) + /// — both in the core Dapper library. + /// + public class FakeDbUdtTypeHandlerTests + { + // ── UdtTypeHandler constructor validation ───────────────────── + + [Fact] + public void UdtTypeHandler_NullName_ThrowsArgumentException() + => Assert.Throws(() => new SqlMapper.UdtTypeHandler(null!)); + + [Fact] + public void UdtTypeHandler_EmptyName_ThrowsArgumentException() + => Assert.Throws(() => new SqlMapper.UdtTypeHandler("")); + + // ── ITypeHandler.Parse ──────────────────────────────────────── + + [Fact] + public void UdtTypeHandler_Parse_DBNull_ReturnsNull() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Point"); + var result = handler.Parse(typeof(object), DBNull.Value); + Assert.Null(result); + } + + [Fact] + public void UdtTypeHandler_Parse_Value_ReturnsValue() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Point"); + var result = handler.Parse(typeof(object), "someGeometry"); + Assert.Equal("someGeometry", result); + } + + // ── ITypeHandler.SetValue — DBNull skips ConfigureUDT ───────── + + [Fact] + public void UdtTypeHandler_SetValue_DBNull_SetsValueOnly() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Point"); + var param = new MinimalDbParameter(); + + handler.SetValue(param, DBNull.Value); + + Assert.Equal(DBNull.Value, param.Value); + } + + // ── ITypeHandler.SetValue — non-null calls ConfigureUDT ─────── + // Uses a plain param (no UdtTypeName prop) → StructuredHelper returns no-op + + [Fact] + public void UdtTypeHandler_SetValue_NonNull_SetsValueAndCallsConfigureUDT() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Point"); + var param = new MinimalDbParameter(); + + handler.SetValue(param, "POINT(1 2)"); + + Assert.Equal("POINT(1 2)", param.Value); + } + + // ── ITypeHandler.SetValue — with UdtTypeName property → IL path ── + + [Fact] + public void UdtTypeHandler_SetValue_WithUdtTypeName_Property_SetsTypeName() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Point"); + var param = new FakeParamWithUdt(); + + handler.SetValue(param, "POINT(3 4)"); + + Assert.Equal("POINT(3 4)", param.Value); + Assert.Equal("dbo.Point", param.UdtTypeName); + } + + // ── ITypeHandler.SetValue — cache hit (second call with same type) ── + + [Fact] + public void UdtTypeHandler_SetValue_SecondCall_UsesCachedDelegate() + { + SqlMapper.ITypeHandler handler = new SqlMapper.UdtTypeHandler("dbo.Line"); + var p1 = new FakeParamWithUdt(); + var p2 = new FakeParamWithUdt(); + + handler.SetValue(p1, "LINE(0 0,1 1)"); + handler.SetValue(p2, "LINE(2 2,3 3)"); + + Assert.Equal("dbo.Line", p1.UdtTypeName); + Assert.Equal("dbo.Line", p2.UdtTypeName); + } + } + + // ── SqlDataRecordHandler ────────────────────────────────────────── + + public class FakeDbSqlDataRecordHandlerTests + { + // SqlDataRecordHandler is internal, accessed via InternalsVisibleTo("Dapper.Tests") + + [Fact] + public void SqlDataRecordHandler_Parse_AlwaysThrows() + { + var handler = (SqlMapper.ITypeHandler)new SqlDataRecordHandler(); + Assert.Throws(() => + handler.Parse(typeof(IEnumerable), new object())); + } + + [Fact] + public void SqlDataRecordHandler_SetValue_WithNullEnumerable_SetsNull() + { + var handler = (SqlMapper.ITypeHandler)new SqlDataRecordHandler(); + var param = new MinimalDbParameter(); + + // value is not IEnumerable, so "value as IEnumerable" == null + handler.SetValue(param, "not an enumerable"); + + Assert.Null(param.Value); + } + + [Fact] + public void SqlDataRecordHandler_SetValue_WithEmptyList_SetsNull() + { + var handler = (SqlMapper.ITypeHandler)new SqlDataRecordHandler(); + var param = new MinimalDbParameter(); + var emptyList = new List(); // empty → .Any() is false + + handler.SetValue(param, emptyList); + + Assert.Null(param.Value); + } + + [Fact] + public void SqlDataRecordHandler_SetValue_WithRecords_SetsValue() + { + var handler = (SqlMapper.ITypeHandler)new SqlDataRecordHandler(); + var param = new MinimalDbParameter(); + var records = new List { new SimpleDataRecord() }; + + handler.SetValue(param, records); + + Assert.NotNull(param.Value); + } + } + + // ── Helper types ────────────────────────────────────────────────── + + internal class MinimalDbParameter : IDbDataParameter + { + public DbType DbType { get; set; } = DbType.Object; + public ParameterDirection Direction { get; set; } = ParameterDirection.Input; + public bool IsNullable => false; + public string? ParameterName { get; set; } + public string? SourceColumn { get; set; } + public DataRowVersion SourceVersion { get; set; } = DataRowVersion.Default; + public object? Value { get; set; } + public byte Precision { get; set; } + public byte Scale { get; set; } + public int Size { get; set; } + } + + internal class FakeParamWithUdt : MinimalDbParameter + { + public string? UdtTypeName { get; set; } + public int SqlDbType { get; set; } + } + + internal class SimpleDataRecord : IDataRecord + { + public int FieldCount => 0; + public object this[int i] => throw new NotImplementedException(); + public object this[string name] => throw new NotImplementedException(); + public bool GetBoolean(int i) => throw new NotImplementedException(); + public byte GetByte(int i) => throw new NotImplementedException(); + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotImplementedException(); + public char GetChar(int i) => throw new NotImplementedException(); + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotImplementedException(); + public IDataReader GetData(int i) => throw new NotImplementedException(); + public string GetDataTypeName(int i) => throw new NotImplementedException(); + public DateTime GetDateTime(int i) => throw new NotImplementedException(); + public decimal GetDecimal(int i) => throw new NotImplementedException(); + public double GetDouble(int i) => throw new NotImplementedException(); + public Type GetFieldType(int i) => throw new NotImplementedException(); + public float GetFloat(int i) => throw new NotImplementedException(); + public Guid GetGuid(int i) => throw new NotImplementedException(); + public short GetInt16(int i) => throw new NotImplementedException(); + public int GetInt32(int i) => throw new NotImplementedException(); + public long GetInt64(int i) => throw new NotImplementedException(); + public string GetName(int i) => throw new NotImplementedException(); + public int GetOrdinal(string name) => throw new NotImplementedException(); + public string GetString(int i) => throw new NotImplementedException(); + public object GetValue(int i) => throw new NotImplementedException(); + public int GetValues(object[] values) => throw new NotImplementedException(); + public bool IsDBNull(int i) => throw new NotImplementedException(); + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.ValueTuples.cs b/tests/Dapper.Tests/FakeDbTests.ValueTuples.cs new file mode 100644 index 000000000..7835206d4 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.ValueTuples.cs @@ -0,0 +1,150 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public class FakeDbValueTupleTests + { + [Fact] + public void Query_ValueTuple_TwoElements() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 1 }, { "Item2", "Alice" } } + }); + conn.Open(); + + var result = conn.Query<(int, string)>("SELECT 1, 'Alice'").ToList(); + + Assert.Single(result); + Assert.Equal(1, result[0].Item1); + Assert.Equal("Alice", result[0].Item2); + } + + [Fact] + public void Query_ValueTuple_ThreeElements() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 42 }, { "Item2", "hello" }, { "Item3", true } } + }); + conn.Open(); + + var result = conn.Query<(int, string, bool)>("SELECT 42, 'hello', 1").ToList(); + + Assert.Single(result); + Assert.Equal(42, result[0].Item1); + Assert.Equal("hello", result[0].Item2); + Assert.True(result[0].Item3); + } + + [Fact] + public void Query_ValueTuple_MultipleRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 1 }, { "Item2", "A" } }, + new Dictionary { { "Item1", 2 }, { "Item2", "B" } }, + new Dictionary { { "Item1", 3 }, { "Item2", "C" } }, + }); + conn.Open(); + + var result = conn.Query<(int, string)>("SELECT Id, Name FROM T").ToList(); + + Assert.Equal(3, result.Count); + Assert.Equal(1, result[0].Item1); + Assert.Equal(3, result[2].Item1); + } + + [Fact] + public void QueryFirst_ValueTuple_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 5 }, { "Item2", "five" } } + }); + conn.Open(); + + var result = conn.QueryFirst<(int, string)>("SELECT 5, 'five'"); + + Assert.Equal(5, result.Item1); + Assert.Equal("five", result.Item2); + } + + [Fact] + public void QuerySingle_ValueTuple_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 9 }, { "Item2", "nine" } } + }); + conn.Open(); + + var result = conn.QuerySingle<(int, string)>("SELECT 9, 'nine'"); + + Assert.Equal(9, result.Item1); + } + + [Fact] + public void Query_ValueTuple_NullableElement() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 7 }, { "Item2", DBNull.Value } } + }); + conn.Open(); + + var result = conn.QueryFirst<(int, string?)>("SELECT 7, NULL"); + + Assert.Equal(7, result.Item1); + Assert.Null(result.Item2); + } + + [Fact] + public async Task QueryAsync_ValueTuple_Works() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Item1", 11 }, { "Item2", "eleven" } } + }); + conn.Open(); + + var result = (await conn.QueryAsync<(int, string)>("SELECT 11, 'eleven'")).ToList(); + + Assert.Single(result); + Assert.Equal(11, result[0].Item1); + } + + [Fact] + public void Query_ValueTuple_FourElements() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { + { "Item1", 1 }, { "Item2", "a" }, { "Item3", 3.14 }, { "Item4", true } + } + }); + conn.Open(); + + var result = conn.QueryFirst<(int, string, double, bool)>("SELECT ..."); + + Assert.Equal(1, result.Item1); + Assert.Equal("a", result.Item2); + Assert.True(result.Item4); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.WrappedBasicReader.cs b/tests/Dapper.Tests/FakeDbTests.WrappedBasicReader.cs new file mode 100644 index 000000000..8c8eb9a90 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.WrappedBasicReader.cs @@ -0,0 +1,206 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests that trigger WrappedBasicReader by passing a non-DbDataReader IDataReader + /// to Dapper's Parse/GetRowParser methods. + /// WrappedBasicReader wraps IDataReader → DbDataReader for internal Dapper use. + /// + public class FakeDbWrappedBasicReaderTests + { + // A minimal IDataReader that is NOT a DbDataReader + private sealed class MinimalDataReader : IDataReader + { + private readonly IReadOnlyList> _rows; + private int _pos = -1; + + public MinimalDataReader(IReadOnlyList> rows) + { + _rows = rows; + } + + private Dictionary Current => _rows[_pos]; + private List Columns => _rows.Count == 0 + ? new List() + : _rows[0].Keys.ToList(); + + public int FieldCount => _rows.Count == 0 ? 0 : _rows[0].Count; + public bool Read() => ++_pos < _rows.Count; + public bool NextResult() => false; + public bool IsClosed => _pos >= _rows.Count; + public void Close() { _pos = _rows.Count; } + public int Depth => 0; + public int RecordsAffected => -1; + + public string GetName(int i) => Columns[i]; + public int GetOrdinal(string name) => ((List)Columns).IndexOf(name); + public object GetValue(int i) => Current[Columns[i]] ?? DBNull.Value; + public bool IsDBNull(int i) => Current[Columns[i]] is null || Current[Columns[i]] is DBNull; + public object this[int i] => GetValue(i); + public object this[string name] => GetValue(GetOrdinal(name)); + + public int GetValues(object[] values) + { + for (int i = 0; i < FieldCount; i++) + values[i] = GetValue(i); + return FieldCount; + } + + public bool GetBoolean(int i) => (bool)GetValue(i); + public byte GetByte(int i) => (byte)GetValue(i); + public char GetChar(int i) => (char)GetValue(i); + public short GetInt16(int i) => Convert.ToInt16(GetValue(i)); + public int GetInt32(int i) => Convert.ToInt32(GetValue(i)); + public long GetInt64(int i) => Convert.ToInt64(GetValue(i)); + public float GetFloat(int i) => (float)GetValue(i); + public double GetDouble(int i) => Convert.ToDouble(GetValue(i)); + public decimal GetDecimal(int i) => (decimal)GetValue(i); + public DateTime GetDateTime(int i) => (DateTime)GetValue(i); + public Guid GetGuid(int i) => (Guid)GetValue(i); + public string GetString(int i) => (string)GetValue(i); + + public string GetDataTypeName(int i) => GetFieldType(i).Name; + // GetFieldType must work before Read() — use first row metadata + public Type GetFieldType(int i) + { + if (_rows.Count == 0) return typeof(object); + var colName = Columns[i]; + var val = _rows[0][colName]; + return val?.GetType() ?? typeof(object); + } + public DataTable? GetSchemaTable() => null; + + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => 0; + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => 0; + public IDataReader GetData(int i) => throw new NotSupportedException(); + + public void Dispose() => Close(); + } + + private class User { public int Id { get; set; } public string? Name { get; set; } } + + // ── Parse(IDataReader) with non-DbDataReader ─────────────── + + [Fact] + public void Parse_Generic_NonDbDataReader_TriggersWrappedBasicReader() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + + var results = ((IDataReader)reader).Parse().ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal("Alice", results[0].Name); + Assert.Equal("Bob", results[1].Name); + } + + [Fact] + public void Parse_Dynamic_NonDbDataReader_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Charlie" } } + }); + + var results = ((IDataReader)reader).Parse().ToList(); + + Assert.Single(results); + Assert.Equal(5, (int)results[0].Id); + } + + [Fact] + public void Parse_ByType_NonDbDataReader_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 3 }, { "Name", "Dave" } } + }); + + var results = ((IDataReader)reader).Parse(typeof(User)).ToList(); + + Assert.Single(results); + Assert.Equal("Dave", ((User)results[0]).Name); + } + + [Fact] + public void GetRowParser_IDataReader_NonDbDataReader_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 7 }, { "Name", "Eve" } } + }); + + IDataReader idr = reader; + var parser = idr.GetRowParser(); + + Assert.True(idr.Read()); + var user = parser(idr); + Assert.Equal(7, user.Id); + Assert.Equal("Eve", user.Name); + } + + [Fact] + public void GetRowParser_IDataReader_ByType_NonDbDataReader_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 9 }, { "Name", "Frank" } } + }); + + IDataReader idr = reader; + var parser = idr.GetRowParser(typeof(User)); + + Assert.True(idr.Read()); + var user = (User)parser(idr); + Assert.Equal(9, user.Id); + } + + // ── Parse with value type (covers IsValueType branch) ──── + + [Fact] + public void Parse_ValueType_NonDbDataReader_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Val", 42 } }, + new Dictionary { { "Val", 43 } }, + }); + + var results = ((IDataReader)reader).Parse().ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal(42, results[0]); + Assert.Equal(43, results[1]); + } + + // ── Exercise WrappedBasicReader delegate methods ─────────────── + + [Fact] + public void WrappedBasicReader_DelegatesRead_Works() + { + var reader = new MinimalDataReader(new[] + { + new Dictionary { { "Id", 1 } } + }); + + // GetRowParser wraps in WrappedBasicReader + IDataReader idr = reader; + var parser = idr.GetRowParser(typeof(User), + startIndex: 0, length: -1, returnNullIfFirstMissing: false); + + Assert.True(idr.Read()); + var user = parser(idr); + Assert.Equal(1, user.Id); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.WrappedBasicReaderReflection.cs b/tests/Dapper.Tests/FakeDbTests.WrappedBasicReaderReflection.cs new file mode 100644 index 000000000..d07a064d1 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.WrappedBasicReaderReflection.cs @@ -0,0 +1,545 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for WrappedBasicReader internal methods using reflection. + /// WrappedBasicReader is internal sealed, so we must access it via reflection. + /// It wraps a plain IDataReader as a DbDataReader. + /// + public class FakeDbWrappedBasicReaderReflectionTests + { + // MinimalDataReader: a non-DbDataReader IDataReader with various types + private sealed class MultiTypeDataReader : IDataReader + { + private readonly IReadOnlyList> _rows; + private int _pos = -1; + + public MultiTypeDataReader(IReadOnlyList> rows) + { + _rows = rows; + } + + private Dictionary Current => _rows[_pos]; + private List Columns => _rows.Count == 0 + ? new List() + : new List(_rows[0].Keys); + + public int FieldCount => _rows.Count == 0 ? 0 : _rows[0].Count; + public bool Read() => ++_pos < _rows.Count; + public bool NextResult() => false; + public bool IsClosed => _pos >= _rows.Count; + public void Close() { _pos = _rows.Count; } + public int Depth => 0; + public int RecordsAffected => -1; + + public string GetName(int i) => Columns[i]; + public int GetOrdinal(string name) => Columns.IndexOf(name); + public object GetValue(int i) + { + var val = Current[Columns[i]]; + return val ?? DBNull.Value; + } + public bool IsDBNull(int i) => Current[Columns[i]] is null || Current[Columns[i]] is DBNull; + public object this[int i] => GetValue(i); + public object this[string name] => GetValue(GetOrdinal(name)); + + public int GetValues(object[] values) + { + for (int i = 0; i < FieldCount; i++) values[i] = GetValue(i); + return FieldCount; + } + + public bool GetBoolean(int i) => Convert.ToBoolean(Current[Columns[i]]); + public byte GetByte(int i) => Convert.ToByte(Current[Columns[i]]); + public char GetChar(int i) => Convert.ToChar(Current[Columns[i]]); + public short GetInt16(int i) => Convert.ToInt16(Current[Columns[i]]); + public int GetInt32(int i) => Convert.ToInt32(Current[Columns[i]]); + public long GetInt64(int i) => Convert.ToInt64(Current[Columns[i]]); + public float GetFloat(int i) => Convert.ToSingle(Current[Columns[i]]); + public double GetDouble(int i) => Convert.ToDouble(Current[Columns[i]]); + public decimal GetDecimal(int i) => Convert.ToDecimal(Current[Columns[i]]); + public DateTime GetDateTime(int i) => (DateTime)Current[Columns[i]]!; + public Guid GetGuid(int i) => (Guid)Current[Columns[i]]!; + public string GetString(int i) => (string)Current[Columns[i]]!; + + public string GetDataTypeName(int i) => GetFieldType(i).Name; + public Type GetFieldType(int i) + { + if (_rows.Count == 0) return typeof(object); + var val = _rows[0][Columns[i]]; + return val?.GetType() ?? typeof(object); + } + public DataTable? GetSchemaTable() => null; + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => 0; + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => 0; + public IDataReader GetData(int i) => throw new NotSupportedException(); + public void Dispose() => Close(); + } + + private static DbDataReader CreateWrappedBasicReader(IDataReader reader) + { + var assembly = typeof(SqlMapper).Assembly; + var type = assembly.GetType("Dapper.WrappedBasicReader", throwOnError: true)!; + return (DbDataReader)Activator.CreateInstance(type, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, new object[] { reader }, null)!; + } + + private static MultiTypeDataReader MakeReader() => new MultiTypeDataReader(new[] + { + new Dictionary + { + { "BoolVal", true }, + { "ByteVal", (byte)42 }, + { "CharVal", 'Z' }, + { "Int16Val", (short)100 }, + { "Int32Val", 999 }, + { "Int64Val", 1234567890L }, + { "FloatVal", 3.14f }, + { "DoubleVal", 2.718d }, + { "DecimalVal", 1.23m }, + { "DateVal", new DateTime(2024, 1, 15) }, + { "GuidVal", new Guid("12345678-1234-1234-1234-123456789012") }, + { "StringVal", "hello" }, + { "NullVal", null }, + } + }); + + [Fact] + public void WrappedBasicReader_HasRows_ReturnsTrue() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.HasRows); // always true by contract + } + + [Fact] + public void WrappedBasicReader_IsClosed_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.False(wrapped.IsClosed); + } + + [Fact] + public void WrappedBasicReader_Depth_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.Equal(0, wrapped.Depth); + } + + [Fact] + public void WrappedBasicReader_RecordsAffected_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.Equal(-1, wrapped.RecordsAffected); + } + + [Fact] + public void WrappedBasicReader_NextResult_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.False(wrapped.NextResult()); + } + + [Fact] + public void WrappedBasicReader_FieldCount_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.Equal(13, wrapped.FieldCount); + } + + [Fact] + public void WrappedBasicReader_VisibleFieldCount_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.Equal(13, wrapped.VisibleFieldCount); + } + + [Fact] + public void WrappedBasicReader_GetSchemaTable_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + // returns null from our implementation + var table = wrapped.GetSchemaTable(); + // just verify it doesn't throw + } + + [Fact] + public void WrappedBasicReader_Close_Works() + { + var inner = MakeReader(); + var wrapped = CreateWrappedBasicReader(inner); + wrapped.Close(); + // after close the inner should be closed + } + + [Fact] + public void WrappedBasicReader_GetDataTypeName_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var name = wrapped.GetDataTypeName(0); // BoolVal -> "Boolean" + Assert.Equal("Boolean", name); + } + + [Fact] + public void WrappedBasicReader_GetFieldType_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(typeof(bool), wrapped.GetFieldType(0)); + } + + [Fact] + public void WrappedBasicReader_GetName_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal("BoolVal", wrapped.GetName(0)); + } + + [Fact] + public void WrappedBasicReader_GetOrdinal_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(4, wrapped.GetOrdinal("Int32Val")); + } + + [Fact] + public void WrappedBasicReader_GetValue_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(true, wrapped.GetValue(0)); + } + + [Fact] + public void WrappedBasicReader_GetValues_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var values = new object[13]; + var count = wrapped.GetValues(values); + Assert.Equal(13, count); + } + + [Fact] + public void WrappedBasicReader_IsDBNull_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.False(wrapped.IsDBNull(0)); + Assert.True(wrapped.IsDBNull(12)); // NullVal + } + + [Fact] + public void WrappedBasicReader_GetBoolean_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.True(wrapped.GetBoolean(0)); + } + + [Fact] + public void WrappedBasicReader_GetByte_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal((byte)42, wrapped.GetByte(1)); + } + + [Fact] + public void WrappedBasicReader_GetChar_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal('Z', wrapped.GetChar(2)); + } + + [Fact] + public void WrappedBasicReader_GetInt16_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal((short)100, wrapped.GetInt16(3)); + } + + [Fact] + public void WrappedBasicReader_GetInt32_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(999, wrapped.GetInt32(4)); + } + + [Fact] + public void WrappedBasicReader_GetInt64_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(1234567890L, wrapped.GetInt64(5)); + } + + [Fact] + public void WrappedBasicReader_GetFloat_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(3.14f, wrapped.GetFloat(6)); + } + + [Fact] + public void WrappedBasicReader_GetDouble_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(2.718d, wrapped.GetDouble(7)); + } + + [Fact] + public void WrappedBasicReader_GetDecimal_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(1.23m, wrapped.GetDecimal(8)); + } + + [Fact] + public void WrappedBasicReader_GetDateTime_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(new DateTime(2024, 1, 15), wrapped.GetDateTime(9)); + } + + [Fact] + public void WrappedBasicReader_GetGuid_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(new Guid("12345678-1234-1234-1234-123456789012"), wrapped.GetGuid(10)); + } + + [Fact] + public void WrappedBasicReader_GetString_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal("hello", wrapped.GetString(11)); + } + + [Fact] + public void WrappedBasicReader_GetFieldValue_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(999, wrapped.GetFieldValue(4)); + } + + [Fact] + public void WrappedBasicReader_GetFieldValue_Null_ReturnsDefault() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var val = wrapped.GetFieldValue(12); // NullVal + Assert.Null(val); + } + + [Fact] + public async Task WrappedBasicReader_GetFieldValueAsync_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var val = await wrapped.GetFieldValueAsync(4); + Assert.Equal(999, val); + } + + [Fact] + public async Task WrappedBasicReader_IsDBNullAsync_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.False(await wrapped.IsDBNullAsync(0)); + Assert.True(await wrapped.IsDBNullAsync(12)); + } + + [Fact] + public async Task WrappedBasicReader_NextResultAsync_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.False(await wrapped.NextResultAsync(CancellationToken.None)); + } + + [Fact] + public async Task WrappedBasicReader_ReadAsync_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(await wrapped.ReadAsync(CancellationToken.None)); + Assert.False(await wrapped.ReadAsync(CancellationToken.None)); + } + + [Fact] + public void WrappedBasicReader_Indexer_ByInt_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(true, wrapped[0]); + } + + [Fact] + public void WrappedBasicReader_Indexer_ByName_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(999, wrapped["Int32Val"]); + } + + [Fact] + public void WrappedBasicReader_GetProviderSpecificFieldType_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(typeof(bool), wrapped.GetProviderSpecificFieldType(0)); + } + + [Fact] + public void WrappedBasicReader_GetProviderSpecificValue_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Equal(true, wrapped.GetProviderSpecificValue(0)); + } + + [Fact] + public void WrappedBasicReader_GetProviderSpecificValues_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var values = new object[13]; + wrapped.GetProviderSpecificValues(values); + Assert.Equal(true, values[0]); + } + + [Fact] + public void WrappedBasicReader_GetBytes_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var result = wrapped.GetBytes(0, 0, null, 0, 0); + Assert.Equal(0, result); + } + + [Fact] + public void WrappedBasicReader_GetChars_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + var result = wrapped.GetChars(0, 0, null, 0, 0); + Assert.Equal(0, result); + } + + [Fact] + public void WrappedBasicReader_GetStream_Throws() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Throws(() => wrapped.GetStream(0)); + } + + [Fact] + public void WrappedBasicReader_GetTextReader_Throws() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + Assert.True(wrapped.Read()); + Assert.Throws(() => wrapped.GetTextReader(0)); + } + +#if NET5_0_OR_GREATER + [Fact] + public async Task WrappedBasicReader_CloseAsync_Works() + { + var inner = MakeReader(); + var wrapped = CreateWrappedBasicReader(inner); + await wrapped.CloseAsync(); + } + + [Fact] + public async Task WrappedBasicReader_DisposeAsync_Works() + { + var inner = MakeReader(); + var wrapped = CreateWrappedBasicReader(inner); + await wrapped.DisposeAsync(); + } + + [Fact] + public async Task WrappedBasicReader_GetSchemaTableAsync_Works() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + var table = await wrapped.GetSchemaTableAsync(); + // returns null, no throw + } + + [Fact] + public async Task WrappedBasicReader_GetColumnSchemaAsync_Throws() + { + using var inner = MakeReader(); + using var wrapped = CreateWrappedBasicReader(inner); + await Assert.ThrowsAsync(() => + wrapped.GetColumnSchemaAsync()); + } +#endif + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.WrappedReader.cs b/tests/Dapper.Tests/FakeDbTests.WrappedReader.cs new file mode 100644 index 000000000..cdacc7a4d --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.WrappedReader.cs @@ -0,0 +1,270 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Tests for DisposedReader, WrappedBasicReader, and DbWrappedReader. + /// These are internal classes exercised indirectly via ExecuteReader. + /// + public class FakeDbWrappedReaderTests + { + // ── ExecuteReader returns a reader and delegates correctly ───── + + [Fact] + public void ExecuteReader_CanReadRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 }, { "Name", "Alice" } }, + new Dictionary { { "Id", 2 }, { "Name", "Bob" } }, + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT Id, Name FROM Users"); + + Assert.True(reader.Read()); + Assert.Equal(1, reader.GetInt32(reader.GetOrdinal("Id"))); + Assert.Equal("Alice", reader.GetString(reader.GetOrdinal("Name"))); + Assert.True(reader.Read()); + Assert.Equal(2, reader.GetInt32(reader.GetOrdinal("Id"))); + Assert.False(reader.Read()); + } + + [Fact] + public void ExecuteReader_FieldCount_MatchesColumns() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "A", 1 }, { "B", 2 }, { "C", 3 } } + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT A, B, C FROM T"); + Assert.Equal(3, reader.FieldCount); + } + + [Fact] + public void ExecuteReader_GetName_ReturnsColumnName() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "MyColumn", 42 } } + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT MyColumn FROM T"); + Assert.Equal("MyColumn", reader.GetName(0)); + } + + [Fact] + public void ExecuteReader_GetOrdinal_ReturnsIndex() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "First", 1 }, { "Second", 2 } } + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT First, Second FROM T"); + Assert.Equal(0, reader.GetOrdinal("First")); + Assert.Equal(1, reader.GetOrdinal("Second")); + } + + [Fact] + public void ExecuteReader_IsDBNull_ReturnsTrueForNull() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", DBNull.Value } } + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT NULL AS Val"); + reader.Read(); + Assert.True(reader.IsDBNull(0)); + } + + [Fact] + public void ExecuteReader_GetValue_ReturnsRawValue() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Val", 99 } } + }); + conn.Open(); + + using var reader = conn.ExecuteReader("SELECT 99 AS Val"); + reader.Read(); + Assert.Equal(99, reader.GetValue(0)); + } + + // ── async ExecuteReader ─────────────────────────────────────── + + [Fact] + public async Task ExecuteReaderAsync_CanReadRows() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 5 }, { "Name", "Eve" } } + }); + conn.Open(); + + await using var reader = await conn.ExecuteReaderAsync("SELECT Id, Name FROM Users"); + Assert.True(await reader.ReadAsync()); + Assert.Equal(5, reader.GetInt32(reader.GetOrdinal("Id"))); + } + + // ── DisposedReader sentinel ─────────────────────────────────── + + [Fact] + public void DisposedReader_Read_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + Assert.Throws(() => reader.Read()); + } + + [Fact] + public void DisposedReader_GetValue_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + Assert.Throws(() => reader.GetValue(0)); + } + + [Fact] + public void DisposedReader_NextResult_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + Assert.Throws(() => reader.NextResult()); + } + + [Fact] + public void DisposedReader_GetName_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + Assert.Throws(() => reader.GetName(0)); + } + + [Fact] + public void DisposedReader_IsDBNull_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + Assert.Throws(() => reader.IsDBNull(0)); + } + + [Fact] + public void DisposedReader_GetBoolean_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetBoolean(0)); + + [Fact] + public void DisposedReader_GetByte_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetByte(0)); + + [Fact] + public void DisposedReader_GetInt16_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetInt16(0)); + + [Fact] + public void DisposedReader_GetInt32_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetInt32(0)); + + [Fact] + public void DisposedReader_GetInt64_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetInt64(0)); + + [Fact] + public void DisposedReader_GetFloat_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetFloat(0)); + + [Fact] + public void DisposedReader_GetDouble_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetDouble(0)); + + [Fact] + public void DisposedReader_GetDecimal_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetDecimal(0)); + + [Fact] + public void DisposedReader_GetDateTime_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetDateTime(0)); + + [Fact] + public void DisposedReader_GetGuid_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetGuid(0)); + + [Fact] + public void DisposedReader_GetString_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetString(0)); + + [Fact] + public void DisposedReader_GetOrdinal_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetOrdinal("x")); + + [Fact] + public void DisposedReader_GetDataTypeName_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetDataTypeName(0)); + + [Fact] + public void DisposedReader_GetFieldType_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetFieldType(0)); + + [Fact] + public void DisposedReader_GetValues_Throws() + => Assert.Throws(() => DisposedReader.Instance.GetValues(Array.Empty())); + + [Fact] + public async Task DisposedReader_ReadAsync_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + await Assert.ThrowsAsync(() => + reader.ReadAsync(CancellationToken.None)); + } + + [Fact] + public async Task DisposedReader_GetFieldValueAsync_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + await Assert.ThrowsAsync(() => + reader.GetFieldValueAsync(0, CancellationToken.None)); + } + + [Fact] + public async Task DisposedReader_IsDBNullAsync_ThrowsObjectDisposedException() + { + var reader = DisposedReader.Instance; + await Assert.ThrowsAsync(() => + reader.IsDBNullAsync(0, CancellationToken.None)); + } + + // ── Cancellation token honored ──────────────────────────────── + + [Fact] + public async Task ExecuteReaderAsync_WithCancelledToken_UsesCommandDefinition() + { + // Verify CommandDefinition plumbing works (cancellation checked on connection open) + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] { new Dictionary { { "Id", 1 } } }); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Connection is closed — Dapper will try to open it and should respect the token + await Assert.ThrowsAnyAsync(() => + conn.ExecuteReaderAsync(new CommandDefinition("SELECT Id FROM T", cancellationToken: cts.Token))); + } + } +} +#endif diff --git a/tests/Dapper.Tests/FakeDbTests.WrappedReaderCreate.cs b/tests/Dapper.Tests/FakeDbTests.WrappedReaderCreate.cs new file mode 100644 index 000000000..245729912 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.WrappedReaderCreate.cs @@ -0,0 +1,100 @@ +#if !NET481 +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Reflection; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// Covers DbWrappedReader.Create null-cmd path (line 88) and null-reader path (lines 91-92), + /// and IWrappedDataReader explicit interface properties (lines 98, 100). + /// + public class FakeDbWrappedReaderCreateTests + { + private static Type GetDbWrappedReaderType() + => typeof(SqlMapper).Assembly.GetType("Dapper.DbWrappedReader")!; + + // ── Create(null, reader) — returns reader directly (line 88) ── + + [Fact] + public void DbWrappedReader_Create_NullCmd_ReturnsReaderDirectly() + { + var type = GetDbWrappedReaderType(); + var create = type.GetMethod("Create", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(IDbCommand), typeof(DbDataReader) }, + null)!; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + using var reader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + + // null cmd → returns the reader itself, no wrapping + var result = (DbDataReader)create.Invoke(null, new object?[] { null, reader })!; + Assert.Same(reader, result); + } + + // ── Create(cmd, null) — disposes cmd and returns null (lines 91-92) ── + + [Fact] + public void DbWrappedReader_Create_NullReader_DisposesCmd_ReturnsNull() + { + var type = GetDbWrappedReaderType(); + var create = type.GetMethod("Create", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(IDbCommand), typeof(DbDataReader) }, + null)!; + + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + var cmd = conn.CreateCommand(); + + // null reader → cmd.Dispose() is called, returns null + var result = create.Invoke(null, new object?[] { cmd, null }); + Assert.Null(result); + } + + // ── IWrappedDataReader.Reader and .Command (lines 98, 100) ──── + + [Fact] + public void DbWrappedReader_IWrappedDataReader_ReaderAndCommand_Accessible() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueReaderResult(new[] + { + new Dictionary { { "Id", 1 } } + }); + conn.Open(); + using var innerReader = (DbDataReader)conn.ExecuteReader("SELECT Id FROM T"); + var cmd = conn.CreateCommand(); + + var type = GetDbWrappedReaderType(); + var ctor = type.GetConstructor( + BindingFlags.Public | BindingFlags.Instance, null, + new[] { typeof(IDbCommand), typeof(DbDataReader) }, null)!; + + var wrapped = (DbDataReader)ctor.Invoke(new object[] { cmd, innerReader }); + try + { + var iface = (IWrappedDataReader)wrapped; + Assert.NotNull(iface.Reader); + Assert.NotNull(iface.Command); + } + finally + { + wrapped.Dispose(); + } + } + } +} +#endif