diff --git a/src/FluentCommand.Csv/CsvCommandExtensions.cs b/src/FluentCommand.Csv/CsvCommandExtensions.cs index c3fe58d5..437ea9ff 100644 --- a/src/FluentCommand.Csv/CsvCommandExtensions.cs +++ b/src/FluentCommand.Csv/CsvCommandExtensions.cs @@ -7,6 +7,8 @@ using CsvHelper; using CsvHelper.Configuration; +using FluentCommand.Extensions; + using Microsoft.IO; namespace FluentCommand; @@ -257,17 +259,53 @@ private static void WriteValue(IDataReader reader, CsvWriter writer, int index) return; } +#if NET6_0_OR_GREATER + if (type == typeof(DateOnly)) + { + var value = reader.GetValue(index); + var formatted = value.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture); + + writer.WriteField(formatted); + return; + } + + if (type == typeof(TimeOnly)) + { + var value = reader.GetValue(index); + string formatted = value.Second == 0 && value.Millisecond == 0 + ? value.ToString("HH':'mm", CultureInfo.InvariantCulture) + : value.ToString(CultureInfo.InvariantCulture); + + writer.WriteField(formatted); + return; + } +#endif + if (type == typeof(TimeSpan)) { - var value = reader.GetDateTime(index); - writer.WriteField(value); + var value = reader.GetValue(index); + string formatted = value.Seconds == 0 && value.Milliseconds == 0 + ? value.ToString(@"hh\:mm", CultureInfo.InvariantCulture) + : value.ToString(); + + writer.WriteField(formatted); return; } if (type == typeof(DateTime)) { var value = reader.GetDateTime(index); - writer.WriteField(value); + var dataType = reader.GetDataTypeName(index).ToLowerInvariant(); + + if (string.Equals(dataType, "date", StringComparison.OrdinalIgnoreCase)) + { + var formattedDate = value.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture); + writer.WriteField(formattedDate); + } + else + { + writer.WriteField(value); + } return; } diff --git a/src/FluentCommand.Json/JsonCommandExtensions.cs b/src/FluentCommand.Json/JsonCommandExtensions.cs index abab2152..6545c4e9 100644 --- a/src/FluentCommand.Json/JsonCommandExtensions.cs +++ b/src/FluentCommand.Json/JsonCommandExtensions.cs @@ -1,9 +1,13 @@ +using System; using System.Buffers; using System.Data; using System.Data.Common; +using System.Globalization; using System.Text; using System.Text.Json; +using FluentCommand.Extensions; + using Microsoft.IO; namespace FluentCommand; @@ -173,7 +177,6 @@ private static void WriteValue(IDataReader reader, Utf8JsonWriter writer, int in } var type = reader.GetFieldType(index); - if (type == typeof(string)) { var value = reader.GetString(index); @@ -237,17 +240,53 @@ private static void WriteValue(IDataReader reader, Utf8JsonWriter writer, int in return; } +#if NET6_0_OR_GREATER + if (type == typeof(DateOnly)) + { + var value = reader.GetValue(index); + var formatted = value.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture); + + writer.WriteStringValue(formatted); + return; + } + + if (type == typeof(TimeOnly)) + { + var value = reader.GetValue(index); + string formatted = value.Second == 0 && value.Millisecond == 0 + ? value.ToString("HH':'mm", CultureInfo.InvariantCulture) + : value.ToString("HH':'mm':'ss.FFFFFFF", CultureInfo.InvariantCulture); + + writer.WriteStringValue(formatted); + return; + } +#endif + if (type == typeof(TimeSpan)) { - var value = reader.GetDateTime(index); - writer.WriteStringValue(value); + var value = reader.GetValue(index); + string formatted = value.Seconds == 0 && value.Milliseconds == 0 + ? value.ToString(@"hh\:mm", CultureInfo.InvariantCulture) + : value.ToString(); + + writer.WriteStringValue(formatted); return; } if (type == typeof(DateTime)) { var value = reader.GetDateTime(index); - writer.WriteStringValue(value); + var dataType = reader.GetDataTypeName(index).ToLowerInvariant(); + + if (string.Equals(dataType, "date", StringComparison.OrdinalIgnoreCase)) + { + var formattedDate = value.ToString("yyyy'-'MM'-'dd", CultureInfo.InvariantCulture); + writer.WriteStringValue(formattedDate); + } + else + { + writer.WriteStringValue(value); + } return; } diff --git a/test/FluentCommand.Entities/DataType.cs b/test/FluentCommand.Entities/DataType.cs index be48e4de..a829d711 100644 --- a/test/FluentCommand.Entities/DataType.cs +++ b/test/FluentCommand.Entities/DataType.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace FluentCommand.Entities; @@ -6,7 +7,8 @@ namespace FluentCommand.Entities; [Table("DataType", Schema = "dbo")] public class DataType { - public int Id { get; set; } + [Key] + public long Id { get; set; } public string Name { get; set; } diff --git a/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs b/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs index 67b78198..5e663e0b 100644 --- a/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs +++ b/test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs @@ -166,7 +166,7 @@ public async System.Threading.Tasks.Task BuildMergeDataTypeTests() column.IsKey = true; column.CanUpdate = false; - var users = new List + var items = new List { new() { Id = 1, @@ -214,7 +214,7 @@ public async System.Threading.Tasks.Task BuildMergeDataTypeTests() } }; - var listDataReader = new ListDataReader(users); + var listDataReader = new ListDataReader(items); var mergeDataStatement = DataMergeGenerator.BuildMerge(definition, listDataReader); mergeDataStatement.Should().NotBeNullOrEmpty(); diff --git a/test/FluentCommand.SqlServer.Tests/JsonTests.cs b/test/FluentCommand.SqlServer.Tests/JsonTests.cs index 2fe624e2..a99f9be7 100644 --- a/test/FluentCommand.SqlServer.Tests/JsonTests.cs +++ b/test/FluentCommand.SqlServer.Tests/JsonTests.cs @@ -128,4 +128,58 @@ public async Task QuerySelectAsync() json.Should().NotBeNull(); } + + [Fact] + public async Task QueryDataTypeSelectAsync() + { + var session = Services.GetRequiredService(); + session.Should().NotBeNull(); + + var item = new DataType() + { + Id = DateTime.Now.Ticks, + Name = "Test1", + Boolean = false, + Short = 2, + Long = 200, + Float = 200.20F, + Double = 300.35, + Decimal = 456.12M, + 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), + TimeOnly = new TimeOnly(1, 30, 0), + BooleanNull = false, + ShortNull = 2, + LongNull = 200, + FloatNull = 200.20F, + DoubleNull = 300.35, + DecimalNull = 456.12M, + 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), + }; + + await session + .Sql(builder => builder + .Insert() + .Values(item) + ) + .ExecuteAsync(); + + var json = await session + .Sql(builder => builder + .Select() + .OrderBy(p => p.Id) + .Limit(0, 1000) + ) + .QueryJsonAsync(); + + json.Should().NotBeNull(); + } } diff --git a/test/FluentCommand.SqlServer.Tests/Scripts/Script001.Tracker.Schema.sql b/test/FluentCommand.SqlServer.Tests/Scripts/Script001.Tracker.Schema.sql index 24d7f00c..273510df 100644 --- a/test/FluentCommand.SqlServer.Tests/Scripts/Script001.Tracker.Schema.sql +++ b/test/FluentCommand.SqlServer.Tests/Scripts/Script001.Tracker.Schema.sql @@ -146,7 +146,7 @@ CREATE TABLE [dbo].[UserRole] ( IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[DataType]') AND type in (N'U')) CREATE TABLE [dbo].[DataType] ( - [Id] int NOT NULL, + [Id] bigint NOT NULL, [Name] nvarchar(100) NOT NULL, [Boolean] bit NOT NULL, [Short] smallint NOT NULL, @@ -176,7 +176,7 @@ CREATE TABLE [dbo].[DataType] ( ); -- Types -CREATE TYPE [dbo].[UserImportType] AS TABLE +CREATE TYPE [dbo].[UserImportType] AS TABLE ( [EmailAddress] [nvarchar](256) NOT NULL, [DisplayName] [nvarchar](256) NOT NULL, @@ -249,5 +249,5 @@ CREATE INDEX [IX_UserLogin_UserId] ON [dbo].[UserLogin] ([UserId]); -- change tracking -ALTER TABLE [dbo].[User] +ALTER TABLE [dbo].[User] ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF); diff --git a/test/FluentCommand.SqlServer.Tests/Scripts/Script002.Tracker.Data.sql b/test/FluentCommand.SqlServer.Tests/Scripts/Script002.Tracker.Data.sql index ddf02645..9174210f 100644 --- a/test/FluentCommand.SqlServer.Tests/Scripts/Script002.Tracker.Data.sql +++ b/test/FluentCommand.SqlServer.Tests/Scripts/Script002.Tracker.Data.sql @@ -1,103 +1,103 @@ -- Table [dbo].[Priority] data MERGE INTO [dbo].[Priority] AS t -USING +USING ( VALUES - (1, 'High', 'High Priority', 1, 1), - (2, 'Normal', 'Normal Priority', 2, 1), + (1, 'High', 'High Priority', 1, 1), + (2, 'Normal', 'Normal Priority', 2, 1), (3, 'Low', 'Low Priority', 3, 1) -) +) AS s ([Id], [Name], [Description], [DisplayOrder], [IsActive]) ON (t.[Id] = s.[Id]) -WHEN NOT MATCHED BY TARGET THEN +WHEN NOT MATCHED BY TARGET THEN INSERT ([Id], [Name], [Description], [DisplayOrder], [IsActive]) VALUES (s.[Id], s.[Name], s.[Description], s.[DisplayOrder], s.[IsActive]) -WHEN MATCHED THEN +WHEN MATCHED THEN UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description], t.[DisplayOrder] = s.[DisplayOrder], t.[IsActive] = s.[IsActive] OUTPUT $action as [Action]; -- Table [dbo].[Status] data MERGE INTO [dbo].[Status] AS t -USING +USING ( VALUES - (1, 'Not Started', 'Not Starated', 1, 1), - (2, 'In Progress', 'In Progress', 2, 1), - (3, 'Completed', 'Completed', 3, 1), - (4, 'Blocked', 'Blocked', 4, 1), - (5, 'Deferred', 'Deferred', 5, 1), + (1, 'Not Started', 'Not Starated', 1, 1), + (2, 'In Progress', 'In Progress', 2, 1), + (3, 'Completed', 'Completed', 3, 1), + (4, 'Blocked', 'Blocked', 4, 1), + (5, 'Deferred', 'Deferred', 5, 1), (6, 'Done', 'Done', 6, 1) -) +) AS s ([Id], [Name], [Description], [DisplayOrder], [IsActive]) ON (t.[Id] = s.[Id]) -WHEN NOT MATCHED BY TARGET THEN +WHEN NOT MATCHED BY TARGET THEN INSERT ([Id], [Name], [Description], [DisplayOrder], [IsActive]) VALUES (s.[Id], s.[Name], s.[Description], s.[DisplayOrder], s.[IsActive]) -WHEN MATCHED THEN +WHEN MATCHED THEN UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description], t.[DisplayOrder] = s.[DisplayOrder], t.[IsActive] = s.[IsActive] OUTPUT $action as [Action]; -- Table [dbo].[User] data MERGE INTO [dbo].[User] AS t -USING +USING ( VALUES - ('83507c95-0744-e811-bd87-f8633fc30ac7', 'william.adama@battlestar.com', 1, 'William Adama'), - ('490312a6-0744-e811-bd87-f8633fc30ac7', 'laura.roslin@battlestar.com', 1, 'Laura Roslin'), - ('38da04bb-0744-e811-bd87-f8633fc30ac7', 'kara.thrace@battlestar.com', 1, 'Kara Thrace'), - ('589d67c6-0744-e811-bd87-f8633fc30ac7', 'lee.adama@battlestar.com', 1, 'Lee Adama'), - ('118b84d4-0744-e811-bd87-f8633fc30ac7', 'gaius.baltar@battlestar.com', 1, 'Gaius Baltar'), + ('83507c95-0744-e811-bd87-f8633fc30ac7', 'william.adama@battlestar.com', 1, 'William Adama'), + ('490312a6-0744-e811-bd87-f8633fc30ac7', 'laura.roslin@battlestar.com', 1, 'Laura Roslin'), + ('38da04bb-0744-e811-bd87-f8633fc30ac7', 'kara.thrace@battlestar.com', 1, 'Kara Thrace'), + ('589d67c6-0744-e811-bd87-f8633fc30ac7', 'lee.adama@battlestar.com', 1, 'Lee Adama'), + ('118b84d4-0744-e811-bd87-f8633fc30ac7', 'gaius.baltar@battlestar.com', 1, 'Gaius Baltar'), ('fa7515df-0744-e811-bd87-f8633fc30ac7', 'saul.tigh@battlestar.com', 1, 'Saul Tigh') -) +) AS s ([Id], [EmailAddress], [IsEmailAddressConfirmed], [DisplayName]) ON (t.[Id] = s.[Id]) -WHEN NOT MATCHED BY TARGET THEN +WHEN NOT MATCHED BY TARGET THEN INSERT ([Id], [EmailAddress], [IsEmailAddressConfirmed], [DisplayName]) VALUES (s.[Id], s.[EmailAddress], s.[IsEmailAddressConfirmed], s.[DisplayName]) -WHEN MATCHED THEN +WHEN MATCHED THEN UPDATE SET t.[EmailAddress] = s.[EmailAddress], t.[IsEmailAddressConfirmed] = s.[IsEmailAddressConfirmed], t.[DisplayName] = s.[DisplayName] OUTPUT $action as [Action]; -- Table [dbo].[Role] data MERGE INTO [dbo].[Role] AS t -USING +USING ( VALUES - ('b2d78522-0944-e811-bd87-f8633fc30ac7', 'Administrator', 'Administrator'), - ('b3d78522-0944-e811-bd87-f8633fc30ac7', 'Manager', 'Manager'), + ('b2d78522-0944-e811-bd87-f8633fc30ac7', 'Administrator', 'Administrator'), + ('b3d78522-0944-e811-bd87-f8633fc30ac7', 'Manager', 'Manager'), ('acbffa29-0944-e811-bd87-f8633fc30ac7', 'Member', 'Member') -) +) AS s ([Id], [Name], [Description]) ON (t.[Id] = s.[Id]) -WHEN NOT MATCHED BY TARGET THEN +WHEN NOT MATCHED BY TARGET THEN INSERT ([Id], [Name], [Description]) VALUES (s.[Id], s.[Name], s.[Description]) -WHEN MATCHED THEN +WHEN MATCHED THEN UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description] OUTPUT $action as [Action]; -- Table [dbo].[UserRole] data MERGE INTO [dbo].[UserRole] AS t -USING +USING ( VALUES - ('83507c95-0744-e811-bd87-f8633fc30ac7', 'b2d78522-0944-e811-bd87-f8633fc30ac7'), - ('490312a6-0744-e811-bd87-f8633fc30ac7', 'b2d78522-0944-e811-bd87-f8633fc30ac7'), + ('83507c95-0744-e811-bd87-f8633fc30ac7', 'b2d78522-0944-e811-bd87-f8633fc30ac7'), + ('490312a6-0744-e811-bd87-f8633fc30ac7', 'b2d78522-0944-e811-bd87-f8633fc30ac7'), ('fa7515df-0744-e811-bd87-f8633fc30ac7', 'b3d78522-0944-e811-bd87-f8633fc30ac7') -) +) AS s ([UserId], [RoleId]) ON (t.[UserId] = s.[UserId] and t.[RoleId] = s.[RoleId]) -WHEN NOT MATCHED BY TARGET THEN +WHEN NOT MATCHED BY TARGET THEN INSERT ([UserId], [RoleId]) VALUES (s.[UserId], s.[RoleId]) OUTPUT $action as [Action]; diff --git a/test/FluentCommand.SqlServer.Tests/Scripts/Script003.Tracker.StoredProcedure.sql b/test/FluentCommand.SqlServer.Tests/Scripts/Script003.Tracker.StoredProcedure.sql index 03ce3c35..d5840f77 100644 --- a/test/FluentCommand.SqlServer.Tests/Scripts/Script003.Tracker.StoredProcedure.sql +++ b/test/FluentCommand.SqlServer.Tests/Scripts/Script003.Tracker.StoredProcedure.sql @@ -1,4 +1,4 @@ - + IF OBJECT_ID('[dbo].[UserUpsert]') IS NULL BEGIN EXEC('CREATE PROCEDURE [dbo].[UserUpsert] AS SET NOCOUNT ON;') @@ -42,85 +42,85 @@ BEGIN ) AS s ( - [Id], - [EmailAddress], - [IsEmailAddressConfirmed], - [DisplayName], - [PasswordHash], - [ResetHash], - [InviteHash], - [AccessFailedCount], - [LockoutEnabled], - [LockoutEnd], - [LastLogin], + [Id], + [EmailAddress], + [IsEmailAddressConfirmed], + [DisplayName], + [PasswordHash], + [ResetHash], + [InviteHash], + [AccessFailedCount], + [LockoutEnabled], + [LockoutEnd], + [LastLogin], [IsDeleted] ) ON ( t.[Id] = s.[Id] ) - WHEN NOT MATCHED BY TARGET THEN + WHEN NOT MATCHED BY TARGET THEN INSERT ( - [Id], - [EmailAddress], - [IsEmailAddressConfirmed], - [DisplayName], - [PasswordHash], - [ResetHash], - [InviteHash], - [AccessFailedCount], - [LockoutEnabled], - [LockoutEnd], - [LastLogin], + [Id], + [EmailAddress], + [IsEmailAddressConfirmed], + [DisplayName], + [PasswordHash], + [ResetHash], + [InviteHash], + [AccessFailedCount], + [LockoutEnabled], + [LockoutEnd], + [LastLogin], [IsDeleted] ) VALUES ( - s.[Id], - s.[EmailAddress], - s.[IsEmailAddressConfirmed], - s.[DisplayName], - s.[PasswordHash], - s.[ResetHash], - s.[InviteHash], - s.[AccessFailedCount], - s.[LockoutEnabled], - s.[LockoutEnd], - s.[LastLogin], + s.[Id], + s.[EmailAddress], + s.[IsEmailAddressConfirmed], + s.[DisplayName], + s.[PasswordHash], + s.[ResetHash], + s.[InviteHash], + s.[AccessFailedCount], + s.[LockoutEnabled], + s.[LockoutEnd], + s.[LastLogin], s.[IsDeleted] ) WHEN MATCHED THEN UPDATE SET - t.[EmailAddress] = s.[EmailAddress], - t.[IsEmailAddressConfirmed] = s.[IsEmailAddressConfirmed], - t.[DisplayName] = s.[DisplayName], - t.[PasswordHash] = s.[PasswordHash], - t.[ResetHash] = s.[ResetHash], - t.[InviteHash] = s.[InviteHash], - t.[AccessFailedCount] = s.[AccessFailedCount], - t.[LockoutEnabled] = s.[LockoutEnabled], - t.[LockoutEnd] = s.[LockoutEnd], - t.[LastLogin] = s.[LastLogin], + t.[EmailAddress] = s.[EmailAddress], + t.[IsEmailAddressConfirmed] = s.[IsEmailAddressConfirmed], + t.[DisplayName] = s.[DisplayName], + t.[PasswordHash] = s.[PasswordHash], + t.[ResetHash] = s.[ResetHash], + t.[InviteHash] = s.[InviteHash], + t.[AccessFailedCount] = s.[AccessFailedCount], + t.[LockoutEnabled] = s.[LockoutEnabled], + t.[LockoutEnd] = s.[LockoutEnd], + t.[LastLogin] = s.[LastLogin], t.[IsDeleted] = s.[IsDeleted], t.[Updated] = sysutcdatetime() OUTPUT - INSERTED.[Id], - INSERTED.[EmailAddress], - INSERTED.[IsEmailAddressConfirmed], - INSERTED.[DisplayName], - INSERTED.[PasswordHash], - INSERTED.[ResetHash], - INSERTED.[InviteHash], - INSERTED.[AccessFailedCount], - INSERTED.[LockoutEnabled], - INSERTED.[LockoutEnd], - INSERTED.[LastLogin], - INSERTED.[IsDeleted], - INSERTED.[Created], - INSERTED.[CreatedBy], - INSERTED.[Updated], - INSERTED.[UpdatedBy], + INSERTED.[Id], + INSERTED.[EmailAddress], + INSERTED.[IsEmailAddressConfirmed], + INSERTED.[DisplayName], + INSERTED.[PasswordHash], + INSERTED.[ResetHash], + INSERTED.[InviteHash], + INSERTED.[AccessFailedCount], + INSERTED.[LockoutEnabled], + INSERTED.[LockoutEnd], + INSERTED.[LastLogin], + INSERTED.[IsDeleted], + INSERTED.[Created], + INSERTED.[CreatedBy], + INSERTED.[Updated], + INSERTED.[UpdatedBy], INSERTED.[RowVersion], $action AS [ActionType]; @@ -135,9 +135,9 @@ BEGIN END GO - + ALTER PROCEDURE [dbo].[UserListByEmailAddress] - @EmailAddress NVARCHAR(256), + @EmailAddress NVARCHAR(256), @Offset INT = 0, @Size INT = 100, @Total BIGINT OUT @@ -151,24 +151,24 @@ BEGIN WHERE t.EmailAddress LIKE @EmailAddress ); - SELECT - t.[Id], - t.[EmailAddress], - t.[IsEmailAddressConfirmed], - t.[DisplayName], - t.[FirstName], - t.[LastName], - t.[PasswordHash], - t.[ResetHash], - t.[InviteHash], - t.[AccessFailedCount], - t.[LockoutEnabled], - t.[LockoutEnd], - t.[LastLogin], - t.[IsDeleted], - t.[Created], - t.[CreatedBy], - t.[Updated], + SELECT + t.[Id], + t.[EmailAddress], + t.[IsEmailAddressConfirmed], + t.[DisplayName], + t.[FirstName], + t.[LastName], + t.[PasswordHash], + t.[ResetHash], + t.[InviteHash], + t.[AccessFailedCount], + t.[LockoutEnabled], + t.[LockoutEnd], + t.[LastLogin], + t.[IsDeleted], + t.[Created], + t.[CreatedBy], + t.[Updated], t.[UpdatedBy] FROM [dbo].[User] AS t WHERE t.EmailAddress LIKE @EmailAddress @@ -207,7 +207,7 @@ BEGIN SET NOCOUNT OFF; END -GO +GO IF OBJECT_ID('[dbo].[ImportUsers]') IS NULL BEGIN