diff --git a/src/FluentCommand.SqlServer/Merge/DataMergeColumn.cs b/src/FluentCommand.SqlServer/Merge/DataMergeColumn.cs index fefa38b8..57a16470 100644 --- a/src/FluentCommand.SqlServer/Merge/DataMergeColumn.cs +++ b/src/FluentCommand.SqlServer/Merge/DataMergeColumn.cs @@ -72,10 +72,21 @@ public DataMergeColumn() public bool IsKey { get; set; } /// - /// Gets or sets a value indicating whether the column is ignored, not used by merge. + /// Gets or sets a value indicating whether the column is ignored, not used by merge. /// /// /// true if the column is ignored; otherwise, false. /// public bool IsIgnored { get; set; } + + /// + /// Converts to string. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + return $"Source: {SourceColumn}, Target: {TargetColumn}, NativeType: {NativeType}, Key: {IsKey}, Ignored: {IsIgnored}"; + } } diff --git a/src/FluentCommand.SqlServer/Merge/DataMergeDefinition.cs b/src/FluentCommand.SqlServer/Merge/DataMergeDefinition.cs index aced189c..e65aa7c4 100644 --- a/src/FluentCommand.SqlServer/Merge/DataMergeDefinition.cs +++ b/src/FluentCommand.SqlServer/Merge/DataMergeDefinition.cs @@ -1,8 +1,5 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - using FluentCommand.Extensions; +using FluentCommand.Reflection; namespace FluentCommand.Merge; @@ -116,64 +113,29 @@ public static DataMergeDefinition Create() /// The merge definition up auto map to. public static void AutoMap(DataMergeDefinition mergeDefinition) { - var entityType = typeof(TEntity); - var properties = TypeDescriptor.GetProperties(entityType); - - - var tableAttribute = Attribute.GetCustomAttribute(entityType, typeof(TableAttribute)) as TableAttribute; - if (tableAttribute != null) - { - string targetTable = tableAttribute.Name; - if (!string.IsNullOrEmpty(tableAttribute.Schema)) - targetTable = tableAttribute.Schema + "." + targetTable; - - mergeDefinition.TargetTable = targetTable; - } + var typeAccessor = TypeAccessor.GetAccessor(); - if (string.IsNullOrEmpty(mergeDefinition.TargetTable)) - mergeDefinition.TargetTable = entityType.Name; + // don't overwrite existing + if (mergeDefinition.TargetTable.IsNullOrEmpty()) + mergeDefinition.TargetTable = typeAccessor.TableSchema.HasValue() ? $"{typeAccessor.TableSchema}.{typeAccessor.TableName}" : typeAccessor.TableName; - foreach (PropertyDescriptor p in properties) + foreach (var property in typeAccessor.GetProperties()) { - string sourceColumn = p.Name; - string targetColumn = sourceColumn; - string nativeType = SqlTypeMapping.NativeType(p.PropertyType); - - var columnAttribute = p.Attributes - .OfType() - .FirstOrDefault(); - - if (columnAttribute != null) - { - if (columnAttribute.Name.HasValue()) - targetColumn = columnAttribute.Name; - if (columnAttribute.TypeName.HasValue()) - nativeType = columnAttribute.TypeName; - } + string sourceColumn = property.Name; + string targetColumn = property.Column; + string nativeType = property.ColumnType ?? SqlTypeMapping.NativeType(property.MemberType); + // find existing map and update var mergeColumn = mergeDefinition.Columns.FirstOrAdd( m => m.SourceColumn == sourceColumn, () => new DataMergeColumn { SourceColumn = sourceColumn }); mergeColumn.TargetColumn = targetColumn; mergeColumn.NativeType = nativeType; - - var keyAttribute = p.Attributes - .OfType() - .FirstOrDefault(); - - if (keyAttribute != null) - { - mergeColumn.IsKey = true; - mergeColumn.CanUpdate = false; - } - - var ignoreAttribute = p.Attributes - .OfType() - .FirstOrDefault(); - - if (ignoreAttribute != null) - mergeColumn.IsIgnored = true; + mergeColumn.IsKey = property.IsKey; + mergeColumn.CanUpdate = !property.IsKey && !property.IsDatabaseGenerated && !property.IsConcurrencyCheck; + mergeColumn.CanInsert = !property.IsDatabaseGenerated && !property.IsConcurrencyCheck; + mergeColumn.IsIgnored = property.IsNotMapped; } } diff --git a/src/FluentCommand.SqlServer/SqlTypeMapping.cs b/src/FluentCommand.SqlServer/SqlTypeMapping.cs index a280b4a0..070ee4a9 100644 --- a/src/FluentCommand.SqlServer/SqlTypeMapping.cs +++ b/src/FluentCommand.SqlServer/SqlTypeMapping.cs @@ -20,7 +20,11 @@ public static class SqlTypeMapping {typeof(TimeSpan), "time"}, {typeof(DateTime), "datetime2"}, {typeof(DateTimeOffset), "datetimeoffset"}, - {typeof(Guid), "uniqueidentifier"} + {typeof(Guid), "uniqueidentifier"}, + #if NET6_0_OR_GREATER + {typeof(DateOnly), "date"}, + {typeof(TimeOnly), "time"}, + #endif }; /// diff --git a/src/FluentCommand/Reflection/IMemberInformation.cs b/src/FluentCommand/Reflection/IMemberInformation.cs index 48e82baf..d373299b 100644 --- a/src/FluentCommand/Reflection/IMemberInformation.cs +++ b/src/FluentCommand/Reflection/IMemberInformation.cs @@ -33,6 +33,22 @@ public interface IMemberInformation /// string Column { get; } + /// + /// Gets the database provider specific data type of the column the property is mapped to + /// + /// + /// The database provider specific data type of the column the property is mapped to + /// + string ColumnType { get; } + + /// + /// Gets the zero-based order of the column the property is mapped to + /// + /// + /// The zero-based order of the column the property is mapped to + /// + int? ColumnOrder { get; } + /// /// Gets a value indicating that this property is the unique identify for the entity /// diff --git a/src/FluentCommand/Reflection/MemberAccessor.cs b/src/FluentCommand/Reflection/MemberAccessor.cs index e0243db8..2b3b3e54 100644 --- a/src/FluentCommand/Reflection/MemberAccessor.cs +++ b/src/FluentCommand/Reflection/MemberAccessor.cs @@ -72,6 +72,22 @@ protected MemberAccessor(MemberInfo memberInfo) /// public string Column => _columnAttribute.Value?.Name ?? Name; + /// + /// Gets the database provider specific data type of the column the property is mapped to + /// + /// + /// The database provider specific data type of the column the property is mapped to + /// + public string ColumnType => _columnAttribute.Value?.TypeName; + + /// + /// Gets the zero-based order of the column the property is mapped to + /// + /// + /// The zero-based order of the column the property is mapped to + /// + public int? ColumnOrder => _columnAttribute.Value?.Order; + /// /// Gets a value indicating that this property is the unique identify for the entity /// @@ -170,7 +186,7 @@ public override bool Equals(object obj) /// Returns a hash code for this instance. /// /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// public override int GetHashCode() { diff --git a/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs b/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs index 31ed3067..67b78198 100644 --- a/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs +++ b/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs @@ -126,14 +126,38 @@ public void BuildMergeDataTests() Output.WriteLine("MergeStatement:"); Output.WriteLine(sql); } + [Fact] - public void BuildMergeDataTypeTests() + public async System.Threading.Tasks.Task BuildTableSqlTest() { var definition = new DataMergeDefinition(); DataMergeDefinition.AutoMap(definition); definition.Columns.Should().NotBeNullOrEmpty(); + definition.TargetTable = "dbo.DataType"; + + var column = definition.Columns.Find(c => c.SourceColumn == "Id"); + column.Should().NotBeNull(); + column.IsKey = true; + column.CanUpdate = false; + + var tableStatement = DataMergeGenerator.BuildTable(definition); + tableStatement.Should().NotBeNull(); + await Verifier + .Verify(tableStatement) + .UseDirectory("Snapshots") + .AddScrubber(scrubber => scrubber.Replace(definition.TemporaryTable, "#MergeTable")); + + } + + [Fact] + public async System.Threading.Tasks.Task BuildMergeDataTypeTests() + { + var definition = new DataMergeDefinition(); + + DataMergeDefinition.AutoMap(definition); + definition.Columns.Should().NotBeNullOrEmpty(); definition.TargetTable = "dbo.DataType"; var column = definition.Columns.Find(c => c.SourceColumn == "Id"); @@ -144,8 +168,7 @@ public void BuildMergeDataTypeTests() var users = new List { - new DataType - { + new() { Id = 1, Name = "Test1", Boolean = false, @@ -154,8 +177,8 @@ public void BuildMergeDataTypeTests() Float = 200.20F, Double = 300.35, Decimal = 456.12M, - DateTime = DateTime.Now, - DateTimeOffset = DateTimeOffset.Now, + DateTime = new DateTime(2024, 5, 1, 8, 0, 0), + DateTimeOffset = new DateTimeOffset(2024, 5, 1, 8, 0, 0, TimeSpan.FromHours(-6)), Guid = Guid.Empty, TimeSpan = TimeSpan.FromHours(1), DateOnly = new DateOnly(2022, 12, 1), @@ -166,15 +189,14 @@ public void BuildMergeDataTypeTests() FloatNull = 200.20F, DoubleNull = 300.35, DecimalNull = 456.12M, - DateTimeNull = DateTime.Now, - DateTimeOffsetNull = DateTimeOffset.Now, + DateTimeNull = new DateTime(2024, 4, 1, 8, 0, 0), + DateTimeOffsetNull = new DateTimeOffset(2024, 4, 1, 8, 0, 0, TimeSpan.FromHours(-6)), GuidNull = Guid.Empty, TimeSpanNull = TimeSpan.FromHours(1), DateOnlyNull = new DateOnly(2022, 12, 1), TimeOnlyNull = new TimeOnly(1, 30, 0), }, - new DataType - { + new() { Id = 2, Name = "Test2", Boolean = true, @@ -183,8 +205,8 @@ public void BuildMergeDataTypeTests() Float = 600.20F, Double = 700.35, Decimal = 856.12M, - DateTime = DateTime.Now, - DateTimeOffset = DateTimeOffset.Now, + DateTime = new DateTime(2024, 5, 1, 8, 0, 0), + DateTimeOffset = new DateTimeOffset(2024, 5, 1, 8, 0, 0, TimeSpan.FromHours(-6)), Guid = Guid.Empty, TimeSpan = TimeSpan.FromHours(2), DateOnly = new DateOnly(2022, 12, 12), @@ -194,11 +216,35 @@ public void BuildMergeDataTypeTests() var listDataReader = new ListDataReader(users); - var sql = DataMergeGenerator.BuildMerge(definition, listDataReader); - sql.Should().NotBeNullOrEmpty(); + var mergeDataStatement = DataMergeGenerator.BuildMerge(definition, listDataReader); + mergeDataStatement.Should().NotBeNullOrEmpty(); + await Verifier + .Verify(mergeDataStatement) + .UseDirectory("Snapshots") + .AddScrubber(scrubber => scrubber.Replace(definition.TemporaryTable, "#MergeTable")); + } - Output.WriteLine("MergeStatement:"); - Output.WriteLine(sql); + [Fact] + public async System.Threading.Tasks.Task BuildMergeDataTableTests() + { + var definition = new DataMergeDefinition(); + + DataMergeDefinition.AutoMap(definition); + definition.Columns.Should().NotBeNullOrEmpty(); + definition.TargetTable = "dbo.DataType"; + + var column = definition.Columns.Find(c => c.SourceColumn == "Id"); + column.Should().NotBeNull(); + + column.IsKey = true; + column.CanUpdate = false; + + var mergeStatement = DataMergeGenerator.BuildMerge(definition); + mergeStatement.Should().NotBeNull(); + await Verifier + .Verify(mergeStatement) + .UseDirectory("Snapshots") + .AddScrubber(scrubber => scrubber.Replace(definition.TemporaryTable, "#MergeTable")); } [Fact] diff --git a/test/FluentCommand.SqlServer.Tests/FluentCommand.SqlServer.Tests.csproj b/test/FluentCommand.SqlServer.Tests/FluentCommand.SqlServer.Tests.csproj index 8c450334..6cb99f93 100644 --- a/test/FluentCommand.SqlServer.Tests/FluentCommand.SqlServer.Tests.csproj +++ b/test/FluentCommand.SqlServer.Tests/FluentCommand.SqlServer.Tests.csproj @@ -38,6 +38,7 @@ + all runtime; build; native; contentfiles; analyzers diff --git a/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTableTests.verified.txt b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTableTests.verified.txt new file mode 100644 index 00000000..b97fc1f2 --- /dev/null +++ b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTableTests.verified.txt @@ -0,0 +1,124 @@ +MERGE INTO [dbo].[DataType] AS t +USING +( + SELECT + [Id], + [Name], + [Boolean], + [Short], + [Long], + [Float], + [Double], + [Decimal], + [DateTime], + [DateTimeOffset], + [Guid], + [TimeSpan], + [DateOnly], + [TimeOnly], + [BooleanNull], + [ShortNull], + [LongNull], + [FloatNull], + [DoubleNull], + [DecimalNull], + [DateTimeNull], + [DateTimeOffsetNull], + [GuidNull], + [TimeSpanNull], + [DateOnlyNull], + [TimeOnlyNull] + FROM [#MergeTable] +) +AS s +ON +( + t.[Id] = s.[Id] +) +WHEN NOT MATCHED BY TARGET THEN + INSERT + ( + [Id], + [Name], + [Boolean], + [Short], + [Long], + [Float], + [Double], + [Decimal], + [DateTime], + [DateTimeOffset], + [Guid], + [TimeSpan], + [DateOnly], + [TimeOnly], + [BooleanNull], + [ShortNull], + [LongNull], + [FloatNull], + [DoubleNull], + [DecimalNull], + [DateTimeNull], + [DateTimeOffsetNull], + [GuidNull], + [TimeSpanNull], + [DateOnlyNull], + [TimeOnlyNull] + ) + VALUES + ( + s.[Id], + s.[Name], + s.[Boolean], + s.[Short], + s.[Long], + s.[Float], + s.[Double], + s.[Decimal], + s.[DateTime], + s.[DateTimeOffset], + s.[Guid], + s.[TimeSpan], + s.[DateOnly], + s.[TimeOnly], + s.[BooleanNull], + s.[ShortNull], + s.[LongNull], + s.[FloatNull], + s.[DoubleNull], + s.[DecimalNull], + s.[DateTimeNull], + s.[DateTimeOffsetNull], + s.[GuidNull], + s.[TimeSpanNull], + s.[DateOnlyNull], + s.[TimeOnlyNull] + ) +WHEN MATCHED THEN + UPDATE SET + t.[Name] = s.[Name], + t.[Boolean] = s.[Boolean], + t.[Short] = s.[Short], + t.[Long] = s.[Long], + t.[Float] = s.[Float], + t.[Double] = s.[Double], + t.[Decimal] = s.[Decimal], + t.[DateTime] = s.[DateTime], + t.[DateTimeOffset] = s.[DateTimeOffset], + t.[Guid] = s.[Guid], + t.[TimeSpan] = s.[TimeSpan], + t.[DateOnly] = s.[DateOnly], + t.[TimeOnly] = s.[TimeOnly], + t.[BooleanNull] = s.[BooleanNull], + t.[ShortNull] = s.[ShortNull], + t.[LongNull] = s.[LongNull], + t.[FloatNull] = s.[FloatNull], + t.[DoubleNull] = s.[DoubleNull], + t.[DecimalNull] = s.[DecimalNull], + t.[DateTimeNull] = s.[DateTimeNull], + t.[DateTimeOffsetNull] = s.[DateTimeOffsetNull], + t.[GuidNull] = s.[GuidNull], + t.[TimeSpanNull] = s.[TimeSpanNull], + t.[DateOnlyNull] = s.[DateOnlyNull], + t.[TimeOnlyNull] = s.[TimeOnlyNull] +; \ No newline at end of file diff --git a/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTypeTests.verified.txt b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTypeTests.verified.txt new file mode 100644 index 00000000..072d2f14 --- /dev/null +++ b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTypeTests.verified.txt @@ -0,0 +1,102 @@ +MERGE INTO [dbo].[DataType] AS t +USING +( + VALUES + (1, 'Test1', 0, 2, 200, 200.2, 300.35, 456.12, '2024-05-01 08:00:00Z', '2024-05-01 14:00:00Z', '00000000-0000-0000-0000-000000000000', '01:00:00', '2022-12-01', '01:30:00.000000', 0, 2, 200, 200.2, 300.35, 456.12, '2024-04-01 08:00:00Z', '2024-04-01 14:00:00Z', '00000000-0000-0000-0000-000000000000', '01:00:00', '2022-12-01', '01:30:00.000000'), + (2, 'Test2', 1, 3, 400, 600.2, 700.35, 856.12, '2024-05-01 08:00:00Z', '2024-05-01 14:00:00Z', '00000000-0000-0000-0000-000000000000', '02:00:00', '2022-12-12', '06:30:00.000000', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL) +) +AS s +( + [Id], [Name], [Boolean], [Short], [Long], [Float], [Double], [Decimal], [DateTime], [DateTimeOffset], [Guid], [TimeSpan], [DateOnly], [TimeOnly], [BooleanNull], [ShortNull], [LongNull], [FloatNull], [DoubleNull], [DecimalNull], [DateTimeNull], [DateTimeOffsetNull], [GuidNull], [TimeSpanNull], [DateOnlyNull], [TimeOnlyNull] +) +ON +( + t.[Id] = s.[Id] +) +WHEN NOT MATCHED BY TARGET THEN + INSERT + ( + [Id], + [Name], + [Boolean], + [Short], + [Long], + [Float], + [Double], + [Decimal], + [DateTime], + [DateTimeOffset], + [Guid], + [TimeSpan], + [DateOnly], + [TimeOnly], + [BooleanNull], + [ShortNull], + [LongNull], + [FloatNull], + [DoubleNull], + [DecimalNull], + [DateTimeNull], + [DateTimeOffsetNull], + [GuidNull], + [TimeSpanNull], + [DateOnlyNull], + [TimeOnlyNull] + ) + VALUES + ( + s.[Id], + s.[Name], + s.[Boolean], + s.[Short], + s.[Long], + s.[Float], + s.[Double], + s.[Decimal], + s.[DateTime], + s.[DateTimeOffset], + s.[Guid], + s.[TimeSpan], + s.[DateOnly], + s.[TimeOnly], + s.[BooleanNull], + s.[ShortNull], + s.[LongNull], + s.[FloatNull], + s.[DoubleNull], + s.[DecimalNull], + s.[DateTimeNull], + s.[DateTimeOffsetNull], + s.[GuidNull], + s.[TimeSpanNull], + s.[DateOnlyNull], + s.[TimeOnlyNull] + ) +WHEN MATCHED THEN + UPDATE SET + t.[Name] = s.[Name], + t.[Boolean] = s.[Boolean], + t.[Short] = s.[Short], + t.[Long] = s.[Long], + t.[Float] = s.[Float], + t.[Double] = s.[Double], + t.[Decimal] = s.[Decimal], + t.[DateTime] = s.[DateTime], + t.[DateTimeOffset] = s.[DateTimeOffset], + t.[Guid] = s.[Guid], + t.[TimeSpan] = s.[TimeSpan], + t.[DateOnly] = s.[DateOnly], + t.[TimeOnly] = s.[TimeOnly], + t.[BooleanNull] = s.[BooleanNull], + t.[ShortNull] = s.[ShortNull], + t.[LongNull] = s.[LongNull], + t.[FloatNull] = s.[FloatNull], + t.[DoubleNull] = s.[DoubleNull], + t.[DecimalNull] = s.[DecimalNull], + t.[DateTimeNull] = s.[DateTimeNull], + t.[DateTimeOffsetNull] = s.[DateTimeOffsetNull], + t.[GuidNull] = s.[GuidNull], + t.[TimeSpanNull] = s.[TimeSpanNull], + t.[DateOnlyNull] = s.[DateOnlyNull], + t.[TimeOnlyNull] = s.[TimeOnlyNull] +; \ No newline at end of file diff --git a/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildTableSqlTest.verified.txt b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildTableSqlTest.verified.txt new file mode 100644 index 00000000..fd77918c --- /dev/null +++ b/test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildTableSqlTest.verified.txt @@ -0,0 +1,29 @@ +CREATE TABLE [#MergeTable] +( + [Id] int NULL, + [Name] nvarchar(MAX) NULL, + [Boolean] bit NULL, + [Short] smallint NULL, + [Long] bigint NULL, + [Float] real NULL, + [Double] float NULL, + [Decimal] decimal NULL, + [DateTime] datetime2 NULL, + [DateTimeOffset] datetimeoffset NULL, + [Guid] uniqueidentifier NULL, + [TimeSpan] time NULL, + [DateOnly] date NULL, + [TimeOnly] time NULL, + [BooleanNull] bit NULL, + [ShortNull] smallint NULL, + [LongNull] bigint NULL, + [FloatNull] real NULL, + [DoubleNull] float NULL, + [DecimalNull] decimal NULL, + [DateTimeNull] datetime2 NULL, + [DateTimeOffsetNull] datetimeoffset NULL, + [GuidNull] uniqueidentifier NULL, + [TimeSpanNull] time NULL, + [DateOnlyNull] date NULL, + [TimeOnlyNull] time NULL +)