diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index 4966623c..74748791 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed bug: Error when saving an entity without specifying a default value () - Fixed bug: SqlQuery throws exception when using list parameters ([#540](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/540)). - Added support for the YDB retry policy (ADO.NET) and new configuration methods in `YdbDbContextOptionsBuilder`: - `EnableRetryIdempotence()`: enables retries for errors classified as idempotent. You must ensure the operation itself is idempotent. diff --git a/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs index a4de8c3e..f7b28dd1 100644 --- a/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs +++ b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs @@ -45,7 +45,7 @@ public override async Task AcquireDatabaseLockAsync( { await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( AcquireDatabaseLockCommand(), - ((IYdbRelationalConnection)Dependencies.Connection).Clone(), // TODO usage ExecutionContext + Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken @@ -85,9 +85,7 @@ private async Task ReleaseDatabaseLockAsync() try { await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( - ReleaseDatabaseLockCommand(), - ((IYdbRelationalConnection)Dependencies.Connection).Clone() - ).ConfigureAwait(false); + ReleaseDatabaseLockCommand(), Dependencies.Connection).ConfigureAwait(false); return; } @@ -98,10 +96,8 @@ await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( } } - private IReadOnlyList ReleaseDatabaseLockCommand() => - Dependencies.MigrationsSqlGenerator.Generate(new List - { new SqlOperation { Sql = GetDeleteScript(LockKey) } } - ); + private IReadOnlyList ReleaseDatabaseLockCommand() => Dependencies.MigrationsSqlGenerator + .Generate(new List { new SqlOperation { Sql = GetDeleteScript(LockKey) } }); bool IHistoryRepository.CreateIfNotExists() => CreateIfNotExistsAsync().GetAwaiter().GetResult(); @@ -135,13 +131,7 @@ await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( private IReadOnlyList GetCreateIfNotExistsCommands() => Dependencies.MigrationsSqlGenerator.Generate(new List - { - new SqlOperation - { - Sql = GetCreateIfNotExistsScript(), - SuppressTransaction = true - } - }); + { new SqlOperation { Sql = GetCreateIfNotExistsScript(), SuppressTransaction = true } }); public override string GetCreateIfNotExistsScript() => GetCreateScript().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); @@ -179,8 +169,8 @@ private IReadOnlyList SelectHistoryTableCommand() => { new SqlOperation { - Sql = $"SELECT * FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)}" + - $" WHERE '{MigrationIdColumnName}' = '{LockKey}';" + Sql = $"SELECT * FROM {SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} " + + $"WHERE '{MigrationIdColumnName}' = CAST(RandomUuid(0) AS Text)" } }); diff --git a/src/EFCore.Ydb/src/Migrations/YdbMigrationsSqlGenerator.cs b/src/EFCore.Ydb/src/Migrations/YdbMigrationsSqlGenerator.cs index 79c68fce..a3ed9bd1 100644 --- a/src/EFCore.Ydb/src/Migrations/YdbMigrationsSqlGenerator.cs +++ b/src/EFCore.Ydb/src/Migrations/YdbMigrationsSqlGenerator.cs @@ -81,6 +81,11 @@ MigrationCommandListBuilder builder .Append(" ") .Append(columnType) .Append(operation.IsNullable ? string.Empty : " NOT NULL"); + + if (autoincrement == true) + return; + + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); } protected override void CreateTablePrimaryKeyConstraint( diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs index 5cbb8500..61905efd 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs @@ -1,34 +1,21 @@ using System; -using Microsoft.EntityFrameworkCore.Storage; +using Ydb.Sdk.Ado.YdbType; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; -// TODO: Await DateOnly support in Ydb.Sdk -public class YdbDateOnlyTypeMapping : RelationalTypeMapping +public class YdbDateOnlyTypeMapping : YdbTypeMapping { - private const string DateOnlyFormatConst = "{0:yyyy-MM-dd}"; - - public YdbDateOnlyTypeMapping(string storeType) - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(DateOnly)), - storeType, - StoreTypePostfix.None, - System.Data.DbType.Date - ) - ) + public YdbDateOnlyTypeMapping(YdbDbType ydbDbType) : base(typeof(DateOnly), ydbDbType) { } - protected YdbDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) + private YdbDateOnlyTypeMapping(RelationalTypeMappingParameters parameters, YdbDbType ydbDbType) + : base(parameters, ydbDbType) { } - protected override YdbDateOnlyTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters); + protected override YdbDateOnlyTypeMapping Clone(RelationalTypeMappingParameters parameters) => + new(parameters, YdbDbType); - protected override string GenerateNonNullSqlLiteral(object value) - { - var dateOnly = (DateOnly)value; - return $"Date('{dateOnly.ToString(DateOnlyFormatConst)}')"; - } + protected override string SqlLiteralFormatString => $"{YdbDbType}('{{0:yyyy-MM-dd}}')"; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateTimeTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateTimeTypeMapping.cs index 47a37960..59f1ad2e 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateTimeTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateTimeTypeMapping.cs @@ -1,17 +1,27 @@ -using System.Data; -using Microsoft.EntityFrameworkCore.Storage; +using System; +using Ydb.Sdk.Ado.YdbType; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; -public class YdbDateTimeTypeMapping( - string storeType, - DbType? dbType -) : DateTimeTypeMapping(storeType, dbType) +public class YdbDateTimeTypeMapping : YdbTypeMapping { - private const string DateTimeFormatConst = @"{0:yyyy-MM-dd HH\:mm\:ss.fffffff}"; + public YdbDateTimeTypeMapping(YdbDbType ydbDbType) : base(typeof(DateTime), ydbDbType) + { + } - private string StoreTypeLiteral { get; } = storeType; + private YdbDateTimeTypeMapping(RelationalTypeMappingParameters parameters, YdbDbType ydbDbType) + : base(parameters, ydbDbType) + { + } - protected override string SqlLiteralFormatString - => "CAST('" + DateTimeFormatConst + $"' AS {StoreTypeLiteral})"; + protected override YdbDateTimeTypeMapping Clone(RelationalTypeMappingParameters parameters) => + new(parameters, YdbDbType); + + protected override string SqlLiteralFormatString => YdbDbType switch + { + YdbDbType.Timestamp or YdbDbType.Timestamp64 => $@"{YdbDbType}('{{0:yyyy-MM-ddTHH\:mm\:ss.ffffffZ}}')", + YdbDbType.Datetime or YdbDbType.Datetime64 => $@"{YdbDbType}('{{0:yyyy-MM-ddTHH\:mm\:ssZ}}')", + YdbDbType.Date or YdbDbType.Date32 => $"{YdbDbType}('{{0:yyyy-MM-dd}}')", + _ => throw new ArgumentOutOfRangeException(nameof(YdbDbType), YdbDbType, null) + }; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTimeSpanTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTimeSpanTypeMapping.cs new file mode 100644 index 00000000..de40022b --- /dev/null +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTimeSpanTypeMapping.cs @@ -0,0 +1,23 @@ +using System; +using System.Xml; +using Ydb.Sdk.Ado.YdbType; + +namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; + +public class YdbTimeSpanTypeMapping : YdbTypeMapping +{ + public YdbTimeSpanTypeMapping(YdbDbType ydbDbType) : base(typeof(TimeSpan), ydbDbType) + { + } + + private YdbTimeSpanTypeMapping(RelationalTypeMappingParameters parameters, YdbDbType ydbDbType) + : base(parameters, ydbDbType) + { + } + + protected override YdbTimeSpanTypeMapping Clone(RelationalTypeMappingParameters parameters) => + new(parameters, YdbDbType); + + protected override string GenerateNonNullSqlLiteral(object value) => + $"{YdbDbType}('{XmlConvert.ToString((TimeSpan)value)}')"; +} diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTypeMapping.cs new file mode 100644 index 00000000..1c6de2c8 --- /dev/null +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTypeMapping.cs @@ -0,0 +1,41 @@ +using System; +using System.Data.Common; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; +using Ydb.Sdk.Ado; +using Ydb.Sdk.Ado.YdbType; + +namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; + +public abstract class YdbTypeMapping( + RelationalTypeMapping.RelationalTypeMappingParameters parameters, + YdbDbType ydbDbType +) : RelationalTypeMapping(parameters), IYdbTypeMapping +{ + public YdbDbType YdbDbType { get; } = ydbDbType; + + protected YdbTypeMapping( + Type clrType, + YdbDbType ydbDbType, + JsonValueReaderWriter? jsonValueReaderWriter = null + ) : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(clrType, jsonValueReaderWriter: jsonValueReaderWriter), + ydbDbType.ToString() + ), ydbDbType + ) + { + } + + protected override void ConfigureParameter(DbParameter parameter) + { + if (parameter is not YdbParameter ydbParameter) + { + throw new InvalidOperationException( + $"Ydb-specific type mapping {GetType().Name} being used with non-Ydb parameter type {parameter.GetType().Name}"); + } + + base.ConfigureParameter(parameter); + ydbParameter.YdbDbType = YdbDbType; + } +} diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index b01b160e..4d433711 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -38,19 +38,26 @@ RelationalTypeMappingSourceDependencies relationalDependencies private static readonly YdbDecimalTypeMapping Decimal = YdbDecimalTypeMapping.Default; - private static readonly GuidTypeMapping Guid = YdbGuidTypeMapping.Default; + private static readonly GuidTypeMapping Uuid = YdbGuidTypeMapping.Default; private static readonly YdbTextTypeMapping Text = YdbTextTypeMapping.Default; private static readonly YdbBytesTypeMapping Bytes = YdbBytesTypeMapping.Default; private static readonly YdbJsonTypeMapping Json = new("Json", typeof(JsonElement), null); - private static readonly YdbDateOnlyTypeMapping Date = new("Date"); - private static readonly DateTimeTypeMapping DateTime = new("DateTime"); + private static readonly YdbDateOnlyTypeMapping DateDateOnly = new(YdbDbType.Date); + private static readonly YdbDateOnlyTypeMapping Date32DateOnly = new(YdbDbType.Date32); - private static readonly YdbDateTimeTypeMapping Timestamp = new("Timestamp", DbType.DateTime); + private static readonly YdbDateTimeTypeMapping DateDateTime = new(YdbDbType.Date); + private static readonly YdbDateTimeTypeMapping Date32DateTime = new(YdbDbType.Date32); - // TODO: Await interval in Ydb.Sdk - private static readonly TimeSpanTypeMapping Interval = new("Interval", DbType.Object); + private static readonly YdbDateTimeTypeMapping Datetime = new(YdbDbType.Datetime); + private static readonly YdbDateTimeTypeMapping Datetime64 = new(YdbDbType.Datetime64); + + private static readonly YdbDateTimeTypeMapping Timestamp = new(YdbDbType.Timestamp); + private static readonly YdbDateTimeTypeMapping Timestamp64 = new(YdbDbType.Timestamp64); + + private static readonly YdbTimeSpanTypeMapping Interval = new(YdbDbType.Interval); + private static readonly YdbTimeSpanTypeMapping Interval64 = new(YdbDbType.Interval64); #endregion @@ -72,17 +79,20 @@ RelationalTypeMappingSourceDependencies relationalDependencies { "Float", [Float] }, { "Double", [Double] }, - { "Guid", [Guid] }, - - { "Date", [Date] }, - { "DateTime", [DateTime] }, - { "Timestamp", [Timestamp] }, - { "Interval", [Interval] }, + { "Guid", [Uuid] }, { "Text", [Text] }, { "Bytes", [Bytes] }, - { "Json", [Json] } + { "Date", [DateDateTime, DateDateOnly] }, + { "DateTime", [Datetime] }, + { "Timestamp", [Timestamp] }, + { "Interval", [Interval] }, + + { "Date32", [Date32DateTime, Date32DateOnly] }, + { "Datetime64", [Datetime64] }, + { "Timestamp64", [Timestamp64] }, + { "Interval64", [Interval64] } }; private static readonly Dictionary ClrTypeMapping = new() @@ -102,13 +112,13 @@ RelationalTypeMappingSourceDependencies relationalDependencies { typeof(float), Float }, { typeof(double), Double }, - { typeof(Guid), Guid }, + { typeof(Guid), Uuid }, { typeof(string), Text }, { typeof(byte[]), Bytes }, { typeof(JsonElement), Json }, - { typeof(DateOnly), Date }, + { typeof(DateOnly), DateDateOnly }, { typeof(DateTime), Timestamp }, { typeof(TimeSpan), Interval } }; diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/InsertWithDefaultsTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/InsertWithDefaultsTests.cs new file mode 100644 index 00000000..12449645 --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/InsertWithDefaultsTests.cs @@ -0,0 +1,168 @@ +using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Xunit; + +namespace EntityFrameworkCore.Ydb.FunctionalTests; + +public class InsertWithDefaultsTests +{ + [Fact] + public async Task Insert_WritesDefaultedNonNullColumns_Succeeds() + { + await using var testStore = YdbTestStoreFactory.Instance.Create("InsertWithDefaultsTests"); + + await using var dbContext = new TestEntityDbContext(); + await testStore.CleanAsync(dbContext); + + await dbContext.Database.MigrateAsync(); + + dbContext.Entities.AddRange(new TestEntity { Id = 1 }, new TestEntity { Id = 2 }, new TestEntity { Id = 3 }); + await dbContext.SaveChangesAsync(); + + foreach (var entity in dbContext.Entities.ToList()) + { + Assert.True(entity.BoolValue); + Assert.Equal(1, entity.Int8Value); + Assert.Equal(1, entity.Int16Value); + Assert.Equal(1, entity.Int32Value); + Assert.Equal(1, entity.Int64Value); + + Assert.Equal(1, entity.Uint8Value); + Assert.Equal(1, entity.Uint16Value); + Assert.Equal(1u, entity.Uint32Value); + Assert.Equal(1u, entity.Uint64Value); + + Assert.Equal(1, entity.FloatValue); + Assert.Equal(1, entity.DoubleValue); + Assert.Equal(1.00000m, entity.DecimalValue); + + Assert.Equal(Guid.Empty, entity.GuidValue); + + Assert.Equal("text", entity.TextValue); + Assert.Empty(entity.BytesValue); + + Assert.Equal(new DateOnly(1971, 12, 1), entity.DateValue); + Assert.Equal(new DateTime(1971, 12, 1, 0, 0, 0, DateTimeKind.Utc), entity.DateTimeValue); + Assert.Equal(TimeSpan.FromSeconds(1), entity.IntervalValue); + } + } + + public class TestEntityDbContext : DbContext + { + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity(e => + { + e.ToTable("TestEntity"); + e.HasKey(x => x.Id); + e.Property(x => x.BoolValue).HasDefaultValue(true); + e.Property(x => x.Int8Value).HasDefaultValue(1); + e.Property(x => x.Int16Value).HasDefaultValue(1); + e.Property(x => x.Int32Value).HasDefaultValue(1); + e.Property(x => x.Int64Value).HasDefaultValue(1); + e.Property(x => x.Uint8Value).HasDefaultValue(1); + e.Property(x => x.Uint16Value).HasDefaultValue(1); + e.Property(x => x.Uint32Value).HasDefaultValue(1); + e.Property(x => x.Uint64Value).HasDefaultValue(1); + e.Property(x => x.FloatValue).HasDefaultValue(1); + e.Property(x => x.DoubleValue).HasDefaultValue(1); + e.Property(x => x.DecimalValue).HasDefaultValue(1); + e.Property(x => x.GuidValue).HasDefaultValue(Guid.Empty); + e.Property(x => x.TextValue).HasDefaultValue("text"); + e.Property(x => x.BytesValue).HasDefaultValue(Array.Empty()); + e.Property(x => x.DateValue).HasDefaultValue(new DateOnly(1971, 12, 1)); + e.Property(x => x.DateTimeValue).HasDefaultValue(new DateTime(1971, 12, 1, 0, 0, 0, DateTimeKind.Utc)); + e.Property(x => x.IntervalValue).HasDefaultValue(TimeSpan.FromSeconds(1)); + }); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder + .UseYdb("Host=localhost;Port=2136", builder => builder + .DisableRetryOnFailure() + .MigrationsAssembly(typeof(TestEntityMigration).Assembly.FullName)) + .EnableServiceProviderCaching(false) + .LogTo(Console.WriteLine); + } + + public class TestEntity + { + public int Id { get; set; } + + public bool? BoolValue { get; set; } + + public sbyte Int8Value { get; set; } + public short Int16Value { get; set; } + public int Int32Value { get; set; } + public long Int64Value { get; set; } + + public byte Uint8Value { get; set; } + public ushort Uint16Value { get; set; } + public uint Uint32Value { get; set; } + public ulong Uint64Value { get; set; } + + public float FloatValue { get; set; } + public double DoubleValue { get; set; } + [Precision(25, 5)] public decimal DecimalValue { get; set; } + + public Guid GuidValue { get; set; } + + public string TextValue { get; set; } = null!; + public byte[] BytesValue { get; set; } = null!; + + public DateOnly DateValue { get; set; } + public DateTime DateTimeValue { get; set; } + public TimeSpan IntervalValue { get; set; } + } + + [DbContext(typeof(TestEntityDbContext))] + [Migration("InsertWithDefaultsTests_TestEntity")] + private class TestEntityMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) => + migrationBuilder.CreateTable( + name: "TestEntity", + columns: table => new + { + Id = table.Column(type: "Int32", nullable: false), + + BoolValue = table.Column(type: "Bool", nullable: false, defaultValue: true), + + Int8Value = table.Column(type: "Int8", nullable: false, defaultValue: (sbyte)1), + Int16Value = table.Column(type: "Int16", nullable: false, defaultValue: (short)1), + Int32Value = table.Column(type: "Int32", nullable: false, defaultValue: 1), + Int64Value = table.Column(type: "Int64", nullable: false, defaultValue: 1L), + + Uint8Value = table.Column(type: "Uint8", nullable: false, defaultValue: (byte)1), + Uint16Value = table.Column(type: "Uint16", nullable: false, defaultValue: (ushort)1), + Uint32Value = table.Column(type: "Uint32", nullable: false, defaultValue: (uint)1), + Uint64Value = table.Column(type: "Uint64", nullable: false, defaultValue: (ulong)1), + + FloatValue = table.Column(type: "Float", nullable: false, defaultValue: 1f), + DoubleValue = table.Column(type: "Double", nullable: false, defaultValue: 1d), + + DecimalValue = table.Column(type: "Decimal(25, 5)", precision: 25, scale: 5, + nullable: false, defaultValue: 1.0m), + + GuidValue = table.Column(type: "Uuid", nullable: false, defaultValue: Guid.Empty), + + TextValue = table.Column(type: "Text", nullable: false, defaultValue: "text"), + BytesValue = + table.Column(type: "Bytes", nullable: false, defaultValue: Array.Empty()), + + DateValue = table.Column(type: "Date", nullable: false, + defaultValue: new DateOnly(1971, 12, 1)), + DateTimeValue = table.Column(type: "Timestamp", nullable: false, + defaultValue: new DateTime(1971, 12, 1, 0, 0, 0, DateTimeKind.Utc)), + IntervalValue = table.Column(type: "Interval", nullable: false, + defaultValue: TimeSpan.FromSeconds(1)) + }, + constraints: table => { table.PrimaryKey("PK_TestEntity", x => x.Id); } + ); + + protected override void Down(MigrationBuilder migrationBuilder) => + migrationBuilder.DropTable(name: "TestEntity"); + } +} diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index 0fe025e9..db088143 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -38,6 +38,8 @@ public static IEnumerable GetCollectionTestCases() yield return [(string[])["1", "2", "3"]]; yield return [new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 } }]; yield return [(byte[][])[[1, 1], [2, 2], [3, 3]]]; + yield return [new List { new(2002, 2, 24), new(2012, 2, 24), new(2102, 2, 24) }]; + yield return [new DateOnly[] { new(2002, 2, 24), new(2012, 2, 24), new(2102, 2, 24) }]; yield return [new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3) }]; yield return [(DateTime[])[SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3)]]; @@ -71,6 +73,16 @@ public static IEnumerable GetCollectionTestCases() yield return [new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 }, null }]; yield return [(byte[]?[])[[1, 1], [2, 2], [3, 3], null]]; yield return + [ + new List + { new DateOnly(2002, 2, 24), new DateOnly(2012, 2, 24), new DateOnly(2102, 2, 24), null } + ]; + yield return + [ + new DateOnly?[] + { new DateOnly(2002, 2, 24), new DateOnly(2012, 2, 24), new DateOnly(2102, 2, 24), null } + ]; + yield return [ new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3), null } ]; diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index c4745275..97f1151b 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +- Feat ADO.NET: Added support for the `DateOnly` type in `YdbDataReader.GetFieldValue`. + ## v0.25.1 - Fixed bug ADO.NET: `ArgumentOutOfRangeException` when using `YdbParameter` with `YdbDbType = YdbDbType.List | YdbDbType.Unspecified`; diff --git a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs index 0ebb94b6..be374ed4 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs @@ -342,6 +342,13 @@ public override T GetFieldValue(int ordinal) return (T)(object)GetChar(ordinal); } + if (typeof(T) == typeof(DateOnly)) + { + var dateTime = GetDateTime(ordinal); + + return (T)(object)DateOnly.FromDateTime(dateTime); + } + return base.GetFieldValue(ordinal); } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs index fedb0637..ba881be3 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -140,10 +140,19 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime() ydbCommand.Parameters.AddWithValue("dateOnly", new DateOnly(2002, 2, 24)); Assert.Equal(new DateTime(2002, 2, 24), await ydbCommand.ExecuteScalarAsync()); + var ydbDataReader = await ydbCommand.ExecuteReaderAsync(); + await ydbDataReader.ReadAsync(); + Assert.Equal(new DateOnly(2002, 2, 24), ydbDataReader.GetFieldValue(0)); + Assert.False(await ydbDataReader.ReadAsync()); ydbCommand.Parameters.Clear(); ydbCommand.Parameters.AddWithValue("dateOnly", DbType.Date, new DateOnly(2102, 2, 24)); Assert.Equal(new DateTime(2102, 2, 24), await ydbCommand.ExecuteScalarAsync()); + + ydbDataReader = await ydbCommand.ExecuteReaderAsync(); + await ydbDataReader.ReadAsync(); + Assert.Equal(new DateOnly(2102, 2, 24), ydbDataReader.GetFieldValue(0)); + Assert.False(await ydbDataReader.ReadAsync()); } [Theory]