From 93a1e429522bde8028e6d83e783bb8a2287126aa Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 6 Sep 2024 01:20:33 +0200 Subject: [PATCH 01/10] init --- ...er.AOT.Test.Integration.Executables.csproj | 2 +- .../Models/DateOnlyTimeOnlyPoco.cs | 15 ++ .../UserCode/DateOnlyUsage.cs | 22 ++ .../DateOnlyTimeOnlyTests.cs | 42 ++++ .../Setup/IntegrationTestsBase.cs | 3 + .../Interceptors/DateOnly.input.cs | 34 +++ .../Interceptors/DateOnly.output.cs | 190 ++++++++++++++ .../Interceptors/DateOnly.output.netfx.cs | 234 ++++++++++++++++++ .../Interceptors/DateOnly.output.netfx.txt | 46 ++++ .../Interceptors/DateOnly.output.txt | 4 + 10 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 test/Dapper.AOT.Test.Integration.Executables/Models/DateOnlyTimeOnlyPoco.cs create mode 100644 test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs create mode 100644 test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt create mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt diff --git a/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj b/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj index 828f4929..b996bea0 100644 --- a/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj +++ b/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj @@ -1,6 +1,6 @@  - net8.0;net6.0;net48 + net8.0;net6.0 Dapper.AOT.Test.Integration.Executables diff --git a/test/Dapper.AOT.Test.Integration.Executables/Models/DateOnlyTimeOnlyPoco.cs b/test/Dapper.AOT.Test.Integration.Executables/Models/DateOnlyTimeOnlyPoco.cs new file mode 100644 index 00000000..a7e3e683 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/Models/DateOnlyTimeOnlyPoco.cs @@ -0,0 +1,15 @@ +using System; + +namespace Dapper.AOT.Test.Integration.Executables.Models; + +public class DateOnlyTimeOnlyPoco +{ + public const string TableName = "dateOnlyTimeOnly"; + + public static DateOnly SpecificDate => DateOnly.FromDateTime(new DateTime(year: 2022, month: 2, day: 2)); + public static TimeOnly SpecificTime => TimeOnly.FromDateTime(new DateTime(year: 2022, month: 1, day: 1, hour: 10, minute: 11, second: 12)); + + public int Id { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs new file mode 100644 index 00000000..f0266547 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs @@ -0,0 +1,22 @@ +using System; +using System.Data; +using System.Linq; +using Dapper.AOT.Test.Integration.Executables.Models; + +namespace Dapper.AOT.Test.Integration.Executables.UserCode; + +[DapperAot] +public class DateOnlyUsage : IExecutable +{ + public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) + { + var results = connection.Query( + $"select * from {DateOnlyTimeOnlyPoco.TableName} where dateOnly = @date", + new + { + date = DateOnlyTimeOnlyPoco.SpecificDate + }); + + return results.First(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs new file mode 100644 index 00000000..81500bb8 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs @@ -0,0 +1,42 @@ +using System; +using System.Data; +using Dapper.AOT.Test.Integration.Executables.Models; +using Dapper.AOT.Test.Integration.Executables.UserCode; +using Dapper.AOT.Test.Integration.Setup; + +namespace Dapper.AOT.Test.Integration; + +[Collection(SharedPostgresqlClient.Collection)] +public class DateOnlyTimeOnlyTests : IntegrationTestsBase +{ + public DateOnlyTimeOnlyTests(PostgresqlFixture fixture) : base(fixture) + { + } + + protected override void SetupDatabase(IDbConnection dbConnection) + { + base.SetupDatabase(dbConnection); + + var date = $"{DateOnlyTimeOnlyPoco.SpecificDate.Year}-{DateOnlyTimeOnlyPoco.SpecificDate.Month:2}-{DateOnlyTimeOnlyPoco.SpecificDate.Day:2}"; + + dbConnection.Execute($""" + CREATE TABLE IF NOT EXISTS {DateOnlyTimeOnlyPoco.TableName}( + id integer PRIMARY KEY, + dateOnly DATE, + timeOnly TIME + ); + + TRUNCATE {DateOnlyTimeOnlyPoco.TableName}; + + INSERT INTO {DateOnlyTimeOnlyPoco.TableName} (id, dateOnly, timeOnly) + VALUES (1, '{date}', current_timestamp) + """); + } + + [Fact] + public void DateOnly_BasicUsage_InterceptsAndReturnsExpectedData() + { + var result = ExecuteInterceptedUserCode(DbConnection); + Assert.True(result.Id.Equals(1)); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs index 762259de..1cca6776 100644 --- a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs +++ b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; +using Microsoft.Data.SqlClient; namespace Dapper.AOT.Test.Integration.Setup; @@ -28,6 +29,8 @@ public abstract class IntegrationTestsBase protected IntegrationTestsBase(PostgresqlFixture fixture) { + DbConnection = new SqlConnection() // my local test + DbConnection = fixture.NpgsqlConnection; SetupDatabase(DbConnection); } diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs new file mode 100644 index 00000000..2e628cb0 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs @@ -0,0 +1,34 @@ +using System; +using Dapper; +using System.Data.Common; +using System.Threading.Tasks; + +[module: DapperAot] + +public static class Foo +{ + static async Task SomeCode(DbConnection connection) + { + _ = await connection.QueryAsync("select * from users where birth_date = @BirthDate", new + { + BirthDate = DateOnly.FromDateTime(DateTime.Now) + }); + + _ = await connection.QueryAsync("select * from users where birth_date = @BirthDate", new QueryModel + { + BirthDate = DateOnly.FromDateTime(DateTime.Now) + }); + } + + public class QueryModel + { + public DateOnly BirthDate { get; set; } + } + + public class User + { + public int Id { get; set; } + public string Name { get; set; } + public DateOnly BirthDate { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs new file mode 100644 index 00000000..c31264d4 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs @@ -0,0 +1,190 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 12, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: + // parameter map: BirthDate + // returns data: global::Foo.User + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBufferedAsync(param, RowFactory0.Instance)); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 17, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: global::Foo.QueryModel + // parameter map: BirthDate + // returns data: global::Foo.User + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBufferedAsync((global::Foo.QueryModel)param!, RowFactory0.Instance)); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 926444256U when NormalizedEquals(name, "id"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 1 : 4; + break; + case 4237030186U when NormalizedEquals(name, "birthdate"): + token = type == typeof(global::System.DateOnly) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.User Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.User result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.Id = reader.GetInt32(columnOffset); + break; + case 3: + result.Id = GetValue(reader, columnOffset); + break; + case 1: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.BirthDate = reader.GetFieldValue(columnOffset); + break; + case 5: + result.BirthDate = GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { BirthDate = default(global::System.DateOnly) }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "BirthDate"; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.BirthDate); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { BirthDate = default(global::System.DateOnly) }); // expected shape + var ps = cmd.Parameters; + ps[0].Value = AsValue(typed.BirthDate); + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "BirthDate"; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(args.BirthDate); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + ps[0].Value = AsValue(args.BirthDate); + + } + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs new file mode 100644 index 00000000..3f3a36a1 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs @@ -0,0 +1,234 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 12, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: + // parameter map: BirthDate + // returns data: global::Foo.User + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBufferedAsync(param, RowFactory0.Instance)); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 17, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: global::Foo.QueryModel + // parameter map: BirthDate + // returns data: global::Foo.User + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBufferedAsync((global::Foo.QueryModel)param!, RowFactory0.Instance)); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 926444256U when NormalizedEquals(name, "id"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 1 : 4; + break; + case 4237030186U when NormalizedEquals(name, "birthdate"): + token = type == typeof(DateOnly) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.User Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.User result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.Id = reader.GetInt32(columnOffset); + break; + case 3: + result.Id = GetValue(reader, columnOffset); + break; + case 1: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.BirthDate = reader.IsDBNull(columnOffset) ? (DateOnly?)null : reader.GetFieldValue(columnOffset); + break; + case 5: + result.BirthDate = reader.IsDBNull(columnOffset) ? (DateOnly?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { BirthDate = default()! }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "BirthDate"; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.BirthDate); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { BirthDate = default()! }); // expected shape + var ps = cmd.Parameters; + ps[0].Value = AsValue(typed.BirthDate); + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "BirthDate"; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, args.BirthDate); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], args.BirthDate); + + } + public override bool CanPrepare => true; + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} +namespace Dapper.Aot.Generated +{ + /// + /// Contains helpers to properly handle + /// +#if !DAPPERAOT_INTERNAL + file +#endif + static class DbStringHelpers + { + public static void ConfigureDbStringDbParameter( + global::System.Data.Common.DbParameter dbParameter, + global::Dapper.DbString? dbString) + { + if (dbString is null) + { + dbParameter.Value = global::System.DBNull.Value; + return; + } + + // repeating logic from Dapper: + // https://github.com/DapperLib/Dapper/blob/52160dc44699ec7eb5ad57d0dddc6ded4662fcb9/Dapper/DbString.cs#L71 + if (dbString.Length == -1 && dbString.Value is not null && dbString.Value.Length <= global::Dapper.DbString.DefaultLength) + { + dbParameter.Size = global::Dapper.DbString.DefaultLength; + } + else + { + dbParameter.Size = dbString.Length; + } + + dbParameter.DbType = dbString switch + { + { IsAnsi: true, IsFixedLength: true } => global::System.Data.DbType.AnsiStringFixedLength, + { IsAnsi: true, IsFixedLength: false } => global::System.Data.DbType.AnsiString, + { IsAnsi: false, IsFixedLength: true } => global::System.Data.DbType.StringFixedLength, + { IsAnsi: false, IsFixedLength: false } => global::System.Data.DbType.String, + _ => dbParameter.DbType + }; + + dbParameter.Value = dbString.Value as object ?? global::System.DBNull.Value; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt new file mode 100644 index 00000000..add394c1 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt @@ -0,0 +1,46 @@ +Input code has 3 diagnostics from 'Interceptors/DateOnly.input.cs': + +Error CS0103 Interceptors/DateOnly.input.cs L14 C25 +The name 'DateOnly' does not exist in the current context + +Error CS0103 Interceptors/DateOnly.input.cs L19 C25 +The name 'DateOnly' does not exist in the current context + +Error CS0246 Interceptors/DateOnly.input.cs L32 C16 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 2 commands and 1 readers +Output code has 3 diagnostics from 'Interceptors/DateOnly.input.cs': + +Error CS0103 Interceptors/DateOnly.input.cs L14 C25 +The name 'DateOnly' does not exist in the current context + +Error CS0103 Interceptors/DateOnly.input.cs L19 C25 +The name 'DateOnly' does not exist in the current context + +Error CS0246 Interceptors/DateOnly.input.cs L32 C16 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) +Output code has 7 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': + +Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L77 C52 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) + +Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L107 C81 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) + +Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L107 C119 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) + +Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L110 C81 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) + +Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L110 C107 +The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) + +Error CS1031 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L128 C79 +Type expected + +Error CS1031 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L140 C79 +Type expected diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt new file mode 100644 index 00000000..b5a7d042 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 2 commands and 1 readers From cf9b8588a54dd5f96706f35519e59aff6bc1b749 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 10:46:12 +0200 Subject: [PATCH 02/10] investigate PostgreSql expected data read --- src/Dapper.AOT/Internal/CommandUtils.cs | 34 ++++++ src/Dapper.AOT/Properties/AssemblyInfo.cs | 3 + .../DateOnlyTimeOnlyTests.cs | 3 +- .../Setup/IntegrationTestsBase.cs | 2 - .../Integration/DateOnlyTests.cs | 114 ++++++++++++++++++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/Dapper.AOT/Properties/AssemblyInfo.cs create mode 100644 test/Dapper.AOT.Test/Integration/DateOnlyTests.cs diff --git a/src/Dapper.AOT/Internal/CommandUtils.cs b/src/Dapper.AOT/Internal/CommandUtils.cs index a03a9fa1..7b0d59ac 100644 --- a/src/Dapper.AOT/Internal/CommandUtils.cs +++ b/src/Dapper.AOT/Internal/CommandUtils.cs @@ -165,6 +165,40 @@ internal static T As(object? value) DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); return Unsafe.As(ref t); } +#if NET6_0_OR_GREATER + else if (typeof(T) == typeof(DateOnly)) + { + if (value is DateOnly only) return Unsafe.As(ref only); + + DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); + var dateOnly = DateOnly.FromDateTime(t); + return Unsafe.As(ref dateOnly); + } + else if (typeof(T) == typeof(DateOnly?)) + { + DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); + DateOnly? dateOnly = t is null ? null : DateOnly.FromDateTime(t.Value); + return Unsafe.As(ref dateOnly); + } + else if (typeof(T) == typeof(TimeOnly)) + { + if (value is TimeSpan timeSpan) + { + var fromSpan = TimeOnly.FromTimeSpan(timeSpan); + return Unsafe.As(ref fromSpan); + } + + DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); + var timeOnly = TimeOnly.FromDateTime(t); + return Unsafe.As(ref timeOnly); + } + else if (typeof(T) == typeof(TimeOnly?)) + { + DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); + TimeOnly? timeOnly = t is null ? null : TimeOnly.FromDateTime(t.Value); + return Unsafe.As(ref timeOnly); + } +#endif else if (typeof(T) == typeof(Guid) && (s = value as string) is not null) { Guid t = Guid.Parse(s); diff --git a/src/Dapper.AOT/Properties/AssemblyInfo.cs b/src/Dapper.AOT/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..62522625 --- /dev/null +++ b/src/Dapper.AOT/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapper.AOT.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a17ba361da0990b3da23f3c20f2a002242397b452a28f27832d61d49f35edb54a68b98d98557b8a02be79be42142339c7861af309c8917dee972775e2c358dd6b96109a9147987652b25b8dc52e7f61f22a755831674f0a3cea17bef9abb6b23ef1856a02216864a1ffbb04a4c549258d32ba740fe141dad2f298a8130ea56d0")] \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs index 81500bb8..9157b923 100644 --- a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs +++ b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs @@ -37,6 +37,7 @@ timeOnly TIME public void DateOnly_BasicUsage_InterceptsAndReturnsExpectedData() { var result = ExecuteInterceptedUserCode(DbConnection); - Assert.True(result.Id.Equals(1)); + Assert.True(result.Id.Equals(1)); + Assert.True(result.Date.Equals(DateOnlyTimeOnlyPoco.SpecificDate)); } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs index 1cca6776..bc083b93 100644 --- a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs +++ b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs @@ -29,8 +29,6 @@ public abstract class IntegrationTestsBase protected IntegrationTestsBase(PostgresqlFixture fixture) { - DbConnection = new SqlConnection() // my local test - DbConnection = fixture.NpgsqlConnection; SetupDatabase(DbConnection); } diff --git a/test/Dapper.AOT.Test/Integration/DateOnlyTests.cs b/test/Dapper.AOT.Test/Integration/DateOnlyTests.cs new file mode 100644 index 00000000..e429b802 --- /dev/null +++ b/test/Dapper.AOT.Test/Integration/DateOnlyTests.cs @@ -0,0 +1,114 @@ +using System; +using Dapper.Internal; +using Xunit; + +namespace Dapper.AOT.Test.Integration; + +[Collection(SharedPostgresqlClient.Collection)] +public class DateOnlyTests +{ + static DateTime TimeStampConst = new DateTime(year: 2020, month: 2, day: 2, hour: 2, minute: 2, second: 2, kind: DateTimeKind.Utc); + +#if NET6_0_OR_GREATER + static DateOnly DateOnlyConst = new DateOnly(year: 2021, month: 1, day: 1); + static TimeOnly TimeOnlyConst = new TimeOnly(hour: 3, minute: 3, second: 3); +#endif + + private PostgresqlFixture _fixture; + + public DateOnlyTests(PostgresqlFixture fixture) + { + _fixture = fixture; + fixture.NpgsqlConnection.Execute($""" + CREATE TABLE IF NOT EXISTS date_only_table( + type_timestamp timestamp, + type_date date, + type_time time + ); + TRUNCATE date_only_table; + + INSERT INTO date_only_table (type_timestamp, type_date, type_time) + VALUES ('{TimeStampConst.ToString("yyyy-MM-dd HH:mm:ss")}', '2021-01-01', '03:03:03'); + """); + } + + [Fact] + public void ReadTimestamp() + { + using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + cmd.CommandText = "select type_timestamp from date_only_table"; + using var reader = cmd.ExecuteReader(); + + var readResult = reader.Read(); + Assert.True(readResult); + + // DbReader will return `DateTime` here + var value = reader.GetValue(0); + + var dateTime = CommandUtils.As(value); + Assert.Equal(TimeStampConst, dateTime); + +#if NET6_0_OR_GREATER + // DateTime has Date and Time, so it can be cast to DateOnly and TimeOnly + var dateOnly = CommandUtils.As(value); + Assert.Equal(DateOnly.FromDateTime(TimeStampConst), dateOnly); + + var timeOnly = CommandUtils.As(value); + Assert.Equal(TimeOnly.FromDateTime(TimeStampConst), timeOnly); +#endif + } + + [Fact] + public void ReadDateOnly() + { + using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + cmd.CommandText = "select type_date from date_only_table"; + using var reader = cmd.ExecuteReader(); + + var readResult = reader.Read(); + Assert.True(readResult); + + // DbReader will return `DateTime` here + var value = reader.GetValue(0); + + // technically, date can be interpreted as DateTime. + // so lets ensure it is possible to cast to DateTime with default time, but existing year-month-day + var dateTime = CommandUtils.As(value); + Assert.Equal(new DateTime(year: 2021, month: 1, day: 1), dateTime); + +#if NET6_0_OR_GREATER + var dateOnly = CommandUtils.As(value); + Assert.Equal(DateOnlyConst, dateOnly); + + // it is possible to cast, but it returns default timeOnly, because there is no "time" in actual data + // not sure if we need to explicitly throw here to show it's not actual time + var timeOnly = CommandUtils.As(value); + Assert.Equal(default(TimeOnly), timeOnly); +#endif + } + + [Fact] + public void ReadTimeOnly() + { + using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + cmd.CommandText = "select type_time from date_only_table"; + using var reader = cmd.ExecuteReader(); + + var readResult = reader.Read(); + Assert.True(readResult); + + // DbReader will return `TimeSpan` here + var value = reader.GetValue(0); + + // TimeSpan is not a DateTime, so lets ensure it throws on casting to DateTime + Assert.Throws(() => CommandUtils.As(value)); + +#if NET6_0_OR_GREATER + // we cant cast TimeSpan to DateOnly - therefore it's expected to throw + Assert.Throws(() => CommandUtils.As(value)); + + var timeOnly = CommandUtils.As(value); + Assert.Equal(TimeOnlyConst, timeOnly); +#endif + } +} \ No newline at end of file From 7b7ebbbfb524371c098ff2dd7e1506402d6b39bd Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 11:48:42 +0200 Subject: [PATCH 03/10] run tests for MSSQL as well (different types) --- Directory.Packages.props | 79 ++++++++++--------- test/Dapper.AOT.Test/Dapper.AOT.Test.csproj | 1 + ...eOnlyTests.cs => DateOnlyTimeOnlyTests.cs} | 70 +++++++++++----- .../Integration/MsSqlFixture.cs | 55 +++++++++++++ 4 files changed, 148 insertions(+), 57 deletions(-) rename test/Dapper.AOT.Test/Integration/{DateOnlyTests.cs => DateOnlyTimeOnlyTests.cs} (67%) create mode 100644 test/Dapper.AOT.Test/Integration/MsSqlFixture.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 37851bc2..fdbd4efb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,41 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index 663a4053..f447d516 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -56,6 +56,7 @@ + diff --git a/test/Dapper.AOT.Test/Integration/DateOnlyTests.cs b/test/Dapper.AOT.Test/Integration/DateOnlyTimeOnlyTests.cs similarity index 67% rename from test/Dapper.AOT.Test/Integration/DateOnlyTests.cs rename to test/Dapper.AOT.Test/Integration/DateOnlyTimeOnlyTests.cs index e429b802..5aa87d6e 100644 --- a/test/Dapper.AOT.Test/Integration/DateOnlyTests.cs +++ b/test/Dapper.AOT.Test/Integration/DateOnlyTimeOnlyTests.cs @@ -1,25 +1,17 @@ using System; +using System.Data; +using System.Data.Common; using Dapper.Internal; using Xunit; namespace Dapper.AOT.Test.Integration; [Collection(SharedPostgresqlClient.Collection)] -public class DateOnlyTests +public class DateOnlyTimeOnlyPostgreSqlTests : DateOnlyTimeOnlyTests { - static DateTime TimeStampConst = new DateTime(year: 2020, month: 2, day: 2, hour: 2, minute: 2, second: 2, kind: DateTimeKind.Utc); - -#if NET6_0_OR_GREATER - static DateOnly DateOnlyConst = new DateOnly(year: 2021, month: 1, day: 1); - static TimeOnly TimeOnlyConst = new TimeOnly(hour: 3, minute: 3, second: 3); -#endif - - private PostgresqlFixture _fixture; - - public DateOnlyTests(PostgresqlFixture fixture) - { - _fixture = fixture; - fixture.NpgsqlConnection.Execute($""" + public DateOnlyTimeOnlyPostgreSqlTests(PostgresqlFixture postgresqlFixture) : base( + postgresqlFixture.NpgsqlConnection, + $""" CREATE TABLE IF NOT EXISTS date_only_table( type_timestamp timestamp, type_date date, @@ -29,13 +21,55 @@ type_time time INSERT INTO date_only_table (type_timestamp, type_date, type_time) VALUES ('{TimeStampConst.ToString("yyyy-MM-dd HH:mm:ss")}', '2021-01-01', '03:03:03'); - """); + """) + { + } +} + +[Collection(SharedMsSqlClient.Collection)] +public class DateOnlyTimeOnlyMsSqlTests : DateOnlyTimeOnlyTests +{ + public DateOnlyTimeOnlyMsSqlTests(MsSqlFixture msSqlFixture) : base( + msSqlFixture.MsSqlConnection, + $""" + IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='date_only_table' and xtype='U') + BEGIN + CREATE TABLE date_only_table( + type_timestamp datetime, + type_date date, + type_time time + ); + END; + TRUNCATE TABLE date_only_table; + + INSERT INTO date_only_table (type_timestamp, type_date, type_time) + VALUES ('2020-02-02 02:02:02', '2021-01-01', '03:03:03'); + """) + { + } +} + +public abstract class DateOnlyTimeOnlyTests +{ + protected static DateTime TimeStampConst = new DateTime(year: 2020, month: 2, day: 2, hour: 2, minute: 2, second: 2, kind: DateTimeKind.Utc); + +#if NET6_0_OR_GREATER + static DateOnly DateOnlyConst = new DateOnly(year: 2021, month: 1, day: 1); + static TimeOnly TimeOnlyConst = new TimeOnly(hour: 3, minute: 3, second: 3); +#endif + + readonly IDbConnection _dbConnection; + + public DateOnlyTimeOnlyTests(IDbConnection dbConnection, string initSql) + { + _dbConnection = dbConnection; + _dbConnection.Execute(initSql); } [Fact] public void ReadTimestamp() { - using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "select type_timestamp from date_only_table"; using var reader = cmd.ExecuteReader(); @@ -61,7 +95,7 @@ public void ReadTimestamp() [Fact] public void ReadDateOnly() { - using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "select type_date from date_only_table"; using var reader = cmd.ExecuteReader(); @@ -90,7 +124,7 @@ public void ReadDateOnly() [Fact] public void ReadTimeOnly() { - using var cmd = _fixture.NpgsqlConnection.CreateCommand(); + using var cmd = _dbConnection.CreateCommand(); cmd.CommandText = "select type_time from date_only_table"; using var reader = cmd.ExecuteReader(); diff --git a/test/Dapper.AOT.Test/Integration/MsSqlFixture.cs b/test/Dapper.AOT.Test/Integration/MsSqlFixture.cs new file mode 100644 index 00000000..ac318a5f --- /dev/null +++ b/test/Dapper.AOT.Test/Integration/MsSqlFixture.cs @@ -0,0 +1,55 @@ +using Microsoft.Data.SqlClient; +using System.Data; +using System.Threading.Tasks; +using Testcontainers.MsSql; +using Xunit; + +namespace Dapper.AOT.Test.Integration; + +[CollectionDefinition(Collection)] +public class SharedMsSqlClient : ICollectionFixture +{ + public const string Collection = nameof(SharedMsSqlClient); +} + +public sealed class MsSqlFixture : IAsyncLifetime +{ + private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder() + .Build(); + + public string ConnectionString { get; private set; } = null!; + + private SqlConnection? _sharedConnection; + + public SqlConnection MsSqlConnection + => _sharedConnection ??= CreateOpenConnection(); + + public SqlConnection CreateOpenConnection() + { + var conn = new SqlConnection(ConnectionString); + conn.Open(); + return conn; + } + + async Task IAsyncLifetime.InitializeAsync() + { + await _msSqlContainer.StartAsync(); + ConnectionString = _msSqlContainer.GetConnectionString(); + } + + async Task IAsyncLifetime.DisposeAsync() + { + await using (_msSqlContainer) + { + var tmp = _sharedConnection; + _sharedConnection = null; + if (tmp is not null) + { + tmp.Close(); +#if NET6_0_OR_GREATER + await tmp.DisposeAsync(); +#endif + } + } + } +} From 53e3a998f2f886654c7bf57e9171d5648ba76242 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 12:27:37 +0200 Subject: [PATCH 04/10] integration tests work as well! --- .../UserCode/DateOnlyUsage.cs | 8 +---- .../UserCode/DateOnlyUsageWithDateFilter.cs | 23 +++++++++++++ .../UserCode/DateOnlyUsageWithTimeFilter.cs | 23 +++++++++++++ .../DateOnlyTimeOnlyTests.cs | 33 ++++++++++++++----- .../Setup/IntegrationTestsBase.cs | 14 ++++++-- 5 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs create mode 100644 test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs index f0266547..489819ec 100644 --- a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs @@ -10,13 +10,7 @@ public class DateOnlyUsage : IExecutable { public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) { - var results = connection.Query( - $"select * from {DateOnlyTimeOnlyPoco.TableName} where dateOnly = @date", - new - { - date = DateOnlyTimeOnlyPoco.SpecificDate - }); - + var results = connection.Query($"select * from {DateOnlyTimeOnlyPoco.TableName}"); return results.First(); } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs new file mode 100644 index 00000000..f228a819 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Data; +using System.Linq; +using Dapper.AOT.Test.Integration.Executables.Models; + +namespace Dapper.AOT.Test.Integration.Executables.UserCode; + +[DapperAot] +public class DateOnlyUsageWithDateFilter : IExecutable +{ + public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) + { + var results = connection.Query( + $""" + select * from {DateOnlyTimeOnlyPoco.TableName} + where date = @date + """, + new { date = DateOnlyTimeOnlyPoco.SpecificDate } + ); + + return results.First(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs new file mode 100644 index 00000000..e3b769b3 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Data; +using System.Linq; +using Dapper.AOT.Test.Integration.Executables.Models; + +namespace Dapper.AOT.Test.Integration.Executables.UserCode; + +[DapperAot] +public class DateOnlyUsageWithTimeFilter : IExecutable +{ + public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) + { + var results = connection.Query( + $""" + select * from {DateOnlyTimeOnlyPoco.TableName} + where time = @time + """, + new { time = DateOnlyTimeOnlyPoco.SpecificTime } + ); + + return results.First(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs index 9157b923..77e47be1 100644 --- a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs +++ b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs @@ -17,19 +17,17 @@ protected override void SetupDatabase(IDbConnection dbConnection) { base.SetupDatabase(dbConnection); - var date = $"{DateOnlyTimeOnlyPoco.SpecificDate.Year}-{DateOnlyTimeOnlyPoco.SpecificDate.Month:2}-{DateOnlyTimeOnlyPoco.SpecificDate.Day:2}"; - dbConnection.Execute($""" CREATE TABLE IF NOT EXISTS {DateOnlyTimeOnlyPoco.TableName}( id integer PRIMARY KEY, - dateOnly DATE, - timeOnly TIME + date DATE, + time TIME ); TRUNCATE {DateOnlyTimeOnlyPoco.TableName}; - INSERT INTO {DateOnlyTimeOnlyPoco.TableName} (id, dateOnly, timeOnly) - VALUES (1, '{date}', current_timestamp) + INSERT INTO {DateOnlyTimeOnlyPoco.TableName} (id, date, time) + VALUES (1, '{DateOnlyTimeOnlyPoco.SpecificDate.ToString("yyyy-MM-dd")}', '{DateOnlyTimeOnlyPoco.SpecificTime.ToString("HH:mm:ss")}') """); } @@ -37,7 +35,26 @@ timeOnly TIME public void DateOnly_BasicUsage_InterceptsAndReturnsExpectedData() { var result = ExecuteInterceptedUserCode(DbConnection); - Assert.True(result.Id.Equals(1)); - Assert.True(result.Date.Equals(DateOnlyTimeOnlyPoco.SpecificDate)); + Assert.Equal(1, result.Id); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); + } + + [Fact] + public void DateOnly_WithDateFilter_InterceptsAndReturnsExpectedData() + { + var result = ExecuteInterceptedUserCode(DbConnection); + Assert.Equal(1, result.Id); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); + } + + [Fact] + public void DateOnly_WithTimeFilter_InterceptsAndReturnsExpectedData() + { + var result = ExecuteInterceptedUserCode(DbConnection); + Assert.Equal(1, result.Id); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs index bc083b93..35d8607a 100644 --- a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs +++ b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs @@ -78,8 +78,8 @@ protected TResult ExecuteInterceptedUserCode(IDbConnection var mainMethod = type.GetMethod(nameof(IExecutable.Execute), BindingFlags.Public | BindingFlags.Instance); var result = mainMethod!.Invoke(obj: executableInstance, [ dbConnection ]); - Assert.True(interceptorRecorder.WasCalled); - Assert.True(string.IsNullOrEmpty(interceptorRecorder.Diagnostics), userMessage: interceptorRecorder.Diagnostics); + Assert.True(interceptorRecorder.WasCalled, userMessage: "No interception code invoked"); + Assert.True(string.IsNullOrEmpty(interceptorRecorder.Diagnostics), userMessage: $"Expected no diagnostics from interceptorRecorder. Actual: {interceptorRecorder.Diagnostics}"); return (TResult)result!; } @@ -97,6 +97,14 @@ private static string ReadUserSourceCode() // it's very fragile to get user code cs files into output directory (btw we can't remove them from compilation, because we will use them for assertions) // so let's simply get back to test\ dir, and try to find Executables.UserCode from there var testDir = Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetCurrentDirectory())!.FullName)!.FullName)!.FullName); - return File.ReadAllText(Path.Combine(testDir!.FullName, "Dapper.AOT.Test.Integration.Executables", "UserCode", $"{userTypeName}.cs")); + + var sourceCodeFile = Directory + .GetFiles( + path: Path.Combine(testDir!.FullName, "Dapper.AOT.Test.Integration.Executables", "UserCode"), + searchPattern: $"{userTypeName}.cs", + searchOption: SearchOption.AllDirectories) + .First(); + + return File.ReadAllText(sourceCodeFile); } } \ No newline at end of file From 9014ec683a223007e9bdf22b88aa7cda0c5a70ce Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 15:24:23 +0200 Subject: [PATCH 05/10] and more integration tests --- .../DateOnlyTimeOnly/DateOnlyInsert.cs | 30 +++++++++++++++++++ .../DateOnlyTimeOnlyUsage.cs} | 4 +-- .../DateOnlyUsageWithDateFilter.cs | 2 +- .../DateOnlyUsageWithTimeFilter.cs | 2 +- .../DateOnlyTimeOnlyTests.cs | 13 ++++++-- .../Setup/IntegrationTestsBase.cs | 1 + 6 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyInsert.cs rename test/Dapper.AOT.Test.Integration.Executables/UserCode/{DateOnlyUsage.cs => DateOnlyTimeOnly/DateOnlyTimeOnlyUsage.cs} (70%) rename test/Dapper.AOT.Test.Integration.Executables/UserCode/{ => DateOnlyTimeOnly}/DateOnlyUsageWithDateFilter.cs (88%) rename test/Dapper.AOT.Test.Integration.Executables/UserCode/{ => DateOnlyTimeOnly}/DateOnlyUsageWithTimeFilter.cs (88%) diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyInsert.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyInsert.cs new file mode 100644 index 00000000..6e530980 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyInsert.cs @@ -0,0 +1,30 @@ +using System.Data; +using System.Linq; +using Dapper.AOT.Test.Integration.Executables.Models; + +namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly; + +[DapperAot] +public class DateOnlyInsert : IExecutable +{ + public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) + { + connection.Execute( + $""" + insert into {DateOnlyTimeOnlyPoco.TableName}(id, date, time) + values (@id, @date, @time) + on conflict (id) do nothing; + """, + new DateOnlyTimeOnlyPoco { Id = 2, Date = DateOnlyTimeOnlyPoco.SpecificDate, Time = DateOnlyTimeOnlyPoco.SpecificTime } + ); + + var results = connection.Query( + $""" + select * from {DateOnlyTimeOnlyPoco.TableName} + where id = 2 + """ + ); + + return results.First(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyTimeOnlyUsage.cs similarity index 70% rename from test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs rename to test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyTimeOnlyUsage.cs index 489819ec..2ab1685b 100644 --- a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsage.cs +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyTimeOnlyUsage.cs @@ -3,10 +3,10 @@ using System.Linq; using Dapper.AOT.Test.Integration.Executables.Models; -namespace Dapper.AOT.Test.Integration.Executables.UserCode; +namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly; [DapperAot] -public class DateOnlyUsage : IExecutable +public class DateOnlyTimeOnlyUsage : IExecutable { public DateOnlyTimeOnlyPoco Execute(IDbConnection connection) { diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithDateFilter.cs similarity index 88% rename from test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs rename to test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithDateFilter.cs index f228a819..036096ba 100644 --- a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithDateFilter.cs +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithDateFilter.cs @@ -3,7 +3,7 @@ using System.Linq; using Dapper.AOT.Test.Integration.Executables.Models; -namespace Dapper.AOT.Test.Integration.Executables.UserCode; +namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly; [DapperAot] public class DateOnlyUsageWithDateFilter : IExecutable diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithTimeFilter.cs similarity index 88% rename from test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs rename to test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithTimeFilter.cs index e3b769b3..ffdf5f26 100644 --- a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyUsageWithTimeFilter.cs +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DateOnlyTimeOnly/DateOnlyUsageWithTimeFilter.cs @@ -3,7 +3,7 @@ using System.Linq; using Dapper.AOT.Test.Integration.Executables.Models; -namespace Dapper.AOT.Test.Integration.Executables.UserCode; +namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly; [DapperAot] public class DateOnlyUsageWithTimeFilter : IExecutable diff --git a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs index 77e47be1..ba48aa73 100644 --- a/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs +++ b/test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs @@ -1,7 +1,7 @@ using System; using System.Data; using Dapper.AOT.Test.Integration.Executables.Models; -using Dapper.AOT.Test.Integration.Executables.UserCode; +using Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly; using Dapper.AOT.Test.Integration.Setup; namespace Dapper.AOT.Test.Integration; @@ -34,7 +34,7 @@ time TIME [Fact] public void DateOnly_BasicUsage_InterceptsAndReturnsExpectedData() { - var result = ExecuteInterceptedUserCode(DbConnection); + var result = ExecuteInterceptedUserCode(DbConnection); Assert.Equal(1, result.Id); Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); @@ -57,4 +57,13 @@ public void DateOnly_WithTimeFilter_InterceptsAndReturnsExpectedData() Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); } + + [Fact] + public void DateOnly_Inserts_InterceptsAndReturnsExpectedData() + { + var result = ExecuteInterceptedUserCode(DbConnection); + Assert.Equal(2, result.Id); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date); + Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time); + } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs index 35d8607a..3ea3ec41 100644 --- a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs +++ b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs @@ -62,6 +62,7 @@ protected TResult ExecuteInterceptedUserCode(IDbConnection // Additional stuff required by Dapper.AOT generators MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location), // System.Collections + MetadataReference.CreateFromFile(Assembly.Load("System.Collections.Immutable").Location), // System.Collections.Immutable ], options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); From 9f97bb53734a8d2c98dbe56adf41aaf286867b31 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 16:13:01 +0200 Subject: [PATCH 06/10] and filter tests which should not run --- src/Dapper.AOT/Internal/CommandUtils.cs | 6 + test/Dapper.AOT.Test/Helpers/TestFramework.cs | 28 +++ test/Dapper.AOT.Test/InterceptorTests.cs | 34 ++- ...teOnly.input.cs => DateOnly.net6.input.cs} | 0 ...Only.output.cs => DateOnly.net6.output.cs} | 4 +- ...ly.output.txt => DateOnly.net6.output.txt} | 0 .../Interceptors/DateOnly.output.netfx.cs | 234 ------------------ .../Interceptors/DateOnly.output.netfx.txt | 46 ---- 8 files changed, 65 insertions(+), 287 deletions(-) create mode 100644 test/Dapper.AOT.Test/Helpers/TestFramework.cs rename test/Dapper.AOT.Test/Interceptors/{DateOnly.input.cs => DateOnly.net6.input.cs} (100%) rename test/Dapper.AOT.Test/Interceptors/{DateOnly.output.cs => DateOnly.net6.output.cs} (98%) rename test/Dapper.AOT.Test/Interceptors/{DateOnly.output.txt => DateOnly.net6.output.txt} (100%) delete mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs delete mode 100644 test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt diff --git a/src/Dapper.AOT/Internal/CommandUtils.cs b/src/Dapper.AOT/Internal/CommandUtils.cs index 7b0d59ac..00ac9414 100644 --- a/src/Dapper.AOT/Internal/CommandUtils.cs +++ b/src/Dapper.AOT/Internal/CommandUtils.cs @@ -194,6 +194,12 @@ internal static T As(object? value) } else if (typeof(T) == typeof(TimeOnly?)) { + if (value is TimeSpan timeSpan) + { + var fromSpan = TimeOnly.FromTimeSpan(timeSpan); + return Unsafe.As(ref fromSpan); + } + DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); TimeOnly? timeOnly = t is null ? null : TimeOnly.FromDateTime(t.Value); return Unsafe.As(ref timeOnly); diff --git a/test/Dapper.AOT.Test/Helpers/TestFramework.cs b/test/Dapper.AOT.Test/Helpers/TestFramework.cs new file mode 100644 index 00000000..1637cd65 --- /dev/null +++ b/test/Dapper.AOT.Test/Helpers/TestFramework.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Dapper.AOT.Test.Helpers +{ + internal static class TestFramework + { + public static ISet NetVersions + = ((NET[])Enum.GetValues(typeof(NET))) + .Select(static x => x.ToString()) + .ToHashSet(); + + public static NET DetermineNetVersion() + { +#if NET6_0_OR_GREATER + return NET.net6; +#endif + return NET.net48; + } + + public enum NET + { + net48, + net6 + } + } +} diff --git a/test/Dapper.AOT.Test/InterceptorTests.cs b/test/Dapper.AOT.Test/InterceptorTests.cs index 79bfe4e6..12a2d5dd 100644 --- a/test/Dapper.AOT.Test/InterceptorTests.cs +++ b/test/Dapper.AOT.Test/InterceptorTests.cs @@ -1,4 +1,5 @@ -using Dapper.CodeAnalysis; +using Dapper.AOT.Test.Helpers; +using Dapper.CodeAnalysis; using System; using System.Collections.Generic; using System.IO; @@ -14,10 +15,33 @@ public class InterceptorTests : GeneratorTestBase { public InterceptorTests(ITestOutputHelper log) : base(log) { } - public static IEnumerable GetFiles() => - from path in Directory.GetFiles("Interceptors", "*.cs", SearchOption.AllDirectories) - where path.EndsWith(".input.cs", StringComparison.OrdinalIgnoreCase) - select new object[] { path }; + public static IEnumerable GetFiles() + { + var currentNetVersion = TestFramework.DetermineNetVersion(); + + foreach (var path in Directory.GetFiles("Interceptors", "*.cs", SearchOption.AllDirectories)) + { + if (path.EndsWith(".input.cs", StringComparison.OrdinalIgnoreCase)) + { + var fileName = Path.GetFileName(path); + var fileNetVersionStr = fileName.Split('.')[1]; + if (TestFramework.NetVersions.Contains(fileNetVersionStr)) + { + // it has to be the same or greater version + var fileNetVersion = (TestFramework.NET)Enum.Parse(typeof(TestFramework.NET), fileNetVersionStr); + if (currentNetVersion < fileNetVersion) + { + // skip if current version is lower than specified in the input file name + continue; + } + } + + yield return new object[] { path }; + } + } + + yield break; + } [Theory, MemberData(nameof(GetFiles))] public void Test(string path) diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.net6.input.cs similarity index 100% rename from test/Dapper.AOT.Test/Interceptors/DateOnly.input.cs rename to test/Dapper.AOT.Test/Interceptors/DateOnly.net6.input.cs diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.net6.output.cs similarity index 98% rename from test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs rename to test/Dapper.AOT.Test/Interceptors/DateOnly.net6.output.cs index c31264d4..58f943b4 100644 --- a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/DateOnly.net6.output.cs @@ -3,7 +3,7 @@ namespace Dapper.AOT // interceptors must be in a known namespace { file static class DapperGeneratedInterceptors { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 12, 30)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.net6.input.cs", 12, 30)] internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters @@ -19,7 +19,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 17, 30)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.net6.input.cs", 17, 30)] internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt b/test/Dapper.AOT.Test/Interceptors/DateOnly.net6.output.txt similarity index 100% rename from test/Dapper.AOT.Test/Interceptors/DateOnly.output.txt rename to test/Dapper.AOT.Test/Interceptors/DateOnly.net6.output.txt diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs deleted file mode 100644 index 3f3a36a1..00000000 --- a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.cs +++ /dev/null @@ -1,234 +0,0 @@ -#nullable enable -namespace Dapper.AOT // interceptors must be in a known namespace -{ - file static class DapperGeneratedInterceptors - { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 12, 30)] - internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters - // takes parameter: - // parameter map: BirthDate - // returns data: global::Foo.User - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); - global::System.Diagnostics.Debug.Assert(param is not null); - - return global::Dapper.DapperAotExtensions.AsEnumerableAsync( - global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBufferedAsync(param, RowFactory0.Instance)); - - } - - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DateOnly.input.cs", 17, 30)] - internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters - // takes parameter: global::Foo.QueryModel - // parameter map: BirthDate - // returns data: global::Foo.User - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); - global::System.Diagnostics.Debug.Assert(param is not null); - - return global::Dapper.DapperAotExtensions.AsEnumerableAsync( - global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBufferedAsync((global::Foo.QueryModel)param!, RowFactory0.Instance)); - - } - - private class CommonCommandFactory : global::Dapper.CommandFactory - { - public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) - { - var cmd = base.GetCommand(connection, sql, commandType, args); - // apply special per-provider command initialization logic for OracleCommand - if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) - { - cmd0.BindByName = true; - cmd0.InitialLONGFetchSize = -1; - - } - return cmd; - } - - } - - private static readonly CommonCommandFactory DefaultCommandFactory = new(); - - private sealed class RowFactory0 : global::Dapper.RowFactory - { - internal static readonly RowFactory0 Instance = new(); - private RowFactory0() {} - public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) - { - for (int i = 0; i < tokens.Length; i++) - { - int token = -1; - var name = reader.GetName(columnOffset); - var type = reader.GetFieldType(columnOffset); - switch (NormalizedHash(name)) - { - case 926444256U when NormalizedEquals(name, "id"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 2369371622U when NormalizedEquals(name, "name"): - token = type == typeof(string) ? 1 : 4; - break; - case 4237030186U when NormalizedEquals(name, "birthdate"): - token = type == typeof(DateOnly) ? 2 : 5; - break; - - } - tokens[i] = token; - columnOffset++; - - } - return null; - } - public override global::Foo.User Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - global::Foo.User result = new(); - foreach (var token in tokens) - { - switch (token) - { - case 0: - result.Id = reader.GetInt32(columnOffset); - break; - case 3: - result.Id = GetValue(reader, columnOffset); - break; - case 1: - result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - result.BirthDate = reader.IsDBNull(columnOffset) ? (DateOnly?)null : reader.GetFieldValue(columnOffset); - break; - case 5: - result.BirthDate = reader.IsDBNull(columnOffset) ? (DateOnly?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return result; - - } - - } - - private sealed class CommandFactory0 : CommonCommandFactory // - { - internal static readonly CommandFactory0 Instance = new(); - public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) - { - var typed = Cast(args, static () => new { BirthDate = default()! }); // expected shape - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "BirthDate"; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(typed.BirthDate); - ps.Add(p); - - } - public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) - { - var typed = Cast(args, static () => new { BirthDate = default()! }); // expected shape - var ps = cmd.Parameters; - ps[0].Value = AsValue(typed.BirthDate); - - } - - } - - private sealed class CommandFactory1 : CommonCommandFactory - { - internal static readonly CommandFactory1 Instance = new(); - public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) - { - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "BirthDate"; - global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, args.BirthDate); - ps.Add(p); - - } - public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) - { - var ps = cmd.Parameters; - global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], args.BirthDate); - - } - public override bool CanPrepare => true; - - } - - - } -} -namespace System.Runtime.CompilerServices -{ - // this type is needed by the compiler to implement interceptors - it doesn't need to - // come from the runtime itself, though - - [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate - [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] - sealed file class InterceptsLocationAttribute : global::System.Attribute - { - public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) - { - _ = path; - _ = lineNumber; - _ = columnNumber; - } - } -} -namespace Dapper.Aot.Generated -{ - /// - /// Contains helpers to properly handle - /// -#if !DAPPERAOT_INTERNAL - file -#endif - static class DbStringHelpers - { - public static void ConfigureDbStringDbParameter( - global::System.Data.Common.DbParameter dbParameter, - global::Dapper.DbString? dbString) - { - if (dbString is null) - { - dbParameter.Value = global::System.DBNull.Value; - return; - } - - // repeating logic from Dapper: - // https://github.com/DapperLib/Dapper/blob/52160dc44699ec7eb5ad57d0dddc6ded4662fcb9/Dapper/DbString.cs#L71 - if (dbString.Length == -1 && dbString.Value is not null && dbString.Value.Length <= global::Dapper.DbString.DefaultLength) - { - dbParameter.Size = global::Dapper.DbString.DefaultLength; - } - else - { - dbParameter.Size = dbString.Length; - } - - dbParameter.DbType = dbString switch - { - { IsAnsi: true, IsFixedLength: true } => global::System.Data.DbType.AnsiStringFixedLength, - { IsAnsi: true, IsFixedLength: false } => global::System.Data.DbType.AnsiString, - { IsAnsi: false, IsFixedLength: true } => global::System.Data.DbType.StringFixedLength, - { IsAnsi: false, IsFixedLength: false } => global::System.Data.DbType.String, - _ => dbParameter.DbType - }; - - dbParameter.Value = dbString.Value as object ?? global::System.DBNull.Value; - } - } -} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt deleted file mode 100644 index add394c1..00000000 --- a/test/Dapper.AOT.Test/Interceptors/DateOnly.output.netfx.txt +++ /dev/null @@ -1,46 +0,0 @@ -Input code has 3 diagnostics from 'Interceptors/DateOnly.input.cs': - -Error CS0103 Interceptors/DateOnly.input.cs L14 C25 -The name 'DateOnly' does not exist in the current context - -Error CS0103 Interceptors/DateOnly.input.cs L19 C25 -The name 'DateOnly' does not exist in the current context - -Error CS0246 Interceptors/DateOnly.input.cs L32 C16 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) -Generator produced 1 diagnostics: - -Hidden DAP000 L1 C1 -Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 2 commands and 1 readers -Output code has 3 diagnostics from 'Interceptors/DateOnly.input.cs': - -Error CS0103 Interceptors/DateOnly.input.cs L14 C25 -The name 'DateOnly' does not exist in the current context - -Error CS0103 Interceptors/DateOnly.input.cs L19 C25 -The name 'DateOnly' does not exist in the current context - -Error CS0246 Interceptors/DateOnly.input.cs L32 C16 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) -Output code has 7 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': - -Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L77 C52 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) - -Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L107 C81 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) - -Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L107 C119 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) - -Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L110 C81 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) - -Error CS0246 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L110 C107 -The type or namespace name 'DateOnly' could not be found (are you missing a using directive or an assembly reference?) - -Error CS1031 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L128 C79 -Type expected - -Error CS1031 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L140 C79 -Type expected From 7a52d947689c31ff9f7f5964e8c03d01bfd61e70 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 16:17:55 +0200 Subject: [PATCH 07/10] dont change indents --- Directory.Packages.props | 80 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fdbd4efb..7a1acb91 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,42 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e01e14bc235d6a3ee5d7f6224bb349847832d738 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 21 Sep 2024 16:37:20 +0200 Subject: [PATCH 08/10] spaces or tabs? --- Directory.Packages.props | 80 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7a1acb91..6e583e05 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,42 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e3598130532dff1381c88ccb073f85800d5acc13 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sun, 22 Sep 2024 13:54:00 +0200 Subject: [PATCH 09/10] add description of how Integration tests work --- test/Dapper.AOT.Test.Integration/README.md | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/Dapper.AOT.Test.Integration/README.md diff --git a/test/Dapper.AOT.Test.Integration/README.md b/test/Dapper.AOT.Test.Integration/README.md new file mode 100644 index 00000000..b3314245 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/README.md @@ -0,0 +1,29 @@ +# Dapper.AOT.Test - Integration + +This is a project for integration tests against a non-mocked database using generated interceptor bits. + +### Requirements +Make sure you have Docker Desktop running to be able to initialize a container with PostgreSQL db (or other). + +### How to add a new test +1) Add your tests to `Dapper.AOT.Test.Integration` and inherit the [IntegrationTestBase](./Setup/IntegrationTestsBase.cs) class +2) Override `SetupDatabase` with in example creating a table and filling it in with some data to query later +3) Call `IntegrationTestBase.ExecuteInterceptedUserCode()` where: + - `TUsage` is a type specified in [Dapper.AOT.Test.Integration.Executables/UserCode](../Dapper.AOT.Test.Integration.Executables/UserCode) project + and implements [IExecutable](../Dapper.AOT.Test.Integration.Executables/IExecutable.cs) interface. + The method `IExecutable.Execute(DbConnection connection)` will be executed by the framework automatically + - `TPoco` is the return type of `IExecutable.Execute(DbConnection connection)`. `TPoco` has to be also defined in [Dapper.AOT.Test.Integration.Executables/Models](../Dapper.AOT.Test.Integration.Executables/Models) +4) Assert that returned `TPoco` has all the data you expected + +### How test framework works + +- Create compilation with reference to user code ([Dapper.AOT.Test.Integration.Executables](../Dapper.AOT.Test.Integration.Executables)) via `CSharpCompilation.Create()` +- give it to `ISourceGenerator` and get an output compilation +- call `outputCompilation.Emit()` and get an assembly +- create instance of user code via reflection (Activator) - this is where we need the `IExecutable` interface to properly cast the type to it +- call `IExecutable.Execute(DbConnection connection)` passing the connection to container-db +- get the output `object`, cast it back to `TPoco` (also defined in [Dapper.AOT.Test.Integration.Executables](../Dapper.AOT.Test.Integration.Executables)) and return it to the test. + Developer can write assertions based on the returned object + +_note_: SourceGenerator is specifically created with the injected `IInterceptorRecorder`, which gets the stack trace of execution and verifies that generated bits were executed. +This gives a full confidence that Interceptors worked as expected. Also it reports diagnostics if something unexpected occured, and test verifies there are none of those. \ No newline at end of file From 85eaadcfda446c47571f9b36feeca02b59a08ef7 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 4 Oct 2024 12:32:24 +0200 Subject: [PATCH 10/10] fix build warnings --- test/Dapper.AOT.Test/Helpers/TestFramework.cs | 16 ++++++++-------- test/Dapper.AOT.Test/InterceptorTests.cs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/Dapper.AOT.Test/Helpers/TestFramework.cs b/test/Dapper.AOT.Test/Helpers/TestFramework.cs index 1637cd65..107af8a1 100644 --- a/test/Dapper.AOT.Test/Helpers/TestFramework.cs +++ b/test/Dapper.AOT.Test/Helpers/TestFramework.cs @@ -6,23 +6,23 @@ namespace Dapper.AOT.Test.Helpers { internal static class TestFramework { - public static ISet NetVersions - = ((NET[])Enum.GetValues(typeof(NET))) + public static readonly ISet NetVersions + = ((Net[])Enum.GetValues(typeof(Net))) .Select(static x => x.ToString()) .ToHashSet(); - public static NET DetermineNetVersion() + public static Net DetermineNetVersion() { #if NET6_0_OR_GREATER - return NET.net6; + return Net.Net6; #endif - return NET.net48; + return Net.Net48; } - public enum NET + public enum Net { - net48, - net6 + Net48, + Net6 } } } diff --git a/test/Dapper.AOT.Test/InterceptorTests.cs b/test/Dapper.AOT.Test/InterceptorTests.cs index 12a2d5dd..2f863dad 100644 --- a/test/Dapper.AOT.Test/InterceptorTests.cs +++ b/test/Dapper.AOT.Test/InterceptorTests.cs @@ -28,7 +28,7 @@ public static IEnumerable GetFiles() if (TestFramework.NetVersions.Contains(fileNetVersionStr)) { // it has to be the same or greater version - var fileNetVersion = (TestFramework.NET)Enum.Parse(typeof(TestFramework.NET), fileNetVersionStr); + var fileNetVersion = (TestFramework.Net)Enum.Parse(typeof(TestFramework.Net), fileNetVersionStr); if (currentNetVersion < fileNetVersion) { // skip if current version is lower than specified in the input file name