diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 429e3aab..f7443b64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -126,8 +126,8 @@ jobs: - name: Run EntityFrameworkCore.Ydb.QuickStart run: | cd ./examples/EntityFrameworkCore.Ydb.QuickStart - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 dotnet ef migrations add InitialCreate dotnet ef database update dotnet run @@ -135,8 +135,8 @@ jobs: - name: Run EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial run: | cd ./examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 dotnet ef migrations add InitialCreate dotnet ef database update dotnet run diff --git a/examples/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj b/examples/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj index 0622e0c7..f4bc20e2 100644 --- a/examples/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj +++ b/examples/EntityFrameworkCore.Ydb.QuickStart/EntityFrameworkCore.Ydb.QuickStart.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/examples/EntityFrameworkCore.Ydb.QuickStart/README.md b/examples/EntityFrameworkCore.Ydb.QuickStart/README.md index 86b56ec9..511e723e 100644 --- a/examples/EntityFrameworkCore.Ydb.QuickStart/README.md +++ b/examples/EntityFrameworkCore.Ydb.QuickStart/README.md @@ -10,8 +10,8 @@ shows how to get started with EF, define a model, populate it with data and then 2. Create the database: ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 dotnet ef migrations add InitialCreate dotnet ef database update ``` diff --git a/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/AddEntity.Sample.csproj b/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/AddEntity.Sample.csproj index 5b482857..71d7a66c 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/AddEntity.Sample.csproj +++ b/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/AddEntity.Sample.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/README.md b/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/README.md index 4d600f18..f7796263 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/README.md +++ b/examples/EntityFrameworkCore.Ydb.Samples/AddEntity.Sample/README.md @@ -11,8 +11,8 @@ Based on the [tutorial](https://www.csharptutorial.net/entity-framework-core-tut 2. Install the EF Core CLI tool and dependencies (if needed): ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 ``` 3. Create the database and apply migrations: diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/Database.Operations.Tutorial.csproj b/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/Database.Operations.Tutorial.csproj index e9e7ec20..1eabfe76 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/Database.Operations.Tutorial.csproj +++ b/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/Database.Operations.Tutorial.csproj @@ -14,7 +14,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/README.md b/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/README.md index 45a23410..42bceb3d 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/README.md +++ b/examples/EntityFrameworkCore.Ydb.Samples/Database.Operations.Tutorial/README.md @@ -10,8 +10,8 @@ Based on the [tutorial](https://www.csharptutorial.net/entity-framework-core-tut 2. Install the EF Core CLI tool and dependencies (if needed): ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 ``` 3. Create the database and apply migrations: diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/README.md b/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/README.md index b12291c7..71d5d18e 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/README.md +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/README.md @@ -11,8 +11,8 @@ The focus here is to show what database schema (DDL) will be generated by EF Cor 2. Install the EF Core CLI tool (if needed): ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 ``` 3. Add a migration to generate the DDL (if not already present): diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/Schema.ManyToMany.csproj b/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/Schema.ManyToMany.csproj index 4449f9bb..10471c41 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/Schema.ManyToMany.csproj +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.ManyToMany/Schema.ManyToMany.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/README.md b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/README.md index f1a9d02f..456df5c8 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/README.md +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/README.md @@ -10,8 +10,8 @@ The focus here is to show what database schema (DDL) will be generated by EF Cor 2. Install the EF Core CLI tool (if needed): ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 ``` 3. Add a migration to generate the DDL (if not already present): diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/Schema.OneToMany.csproj b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/Schema.OneToMany.csproj index 29520a74..76057cce 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/Schema.OneToMany.csproj +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToMany/Schema.OneToMany.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/README.md b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/README.md index 5609e498..bf444ab8 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/README.md +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/README.md @@ -10,8 +10,8 @@ The focus here is to show what database schema (DDL) will be generated by EF Cor 2. Install the EF Core CLI tool (if needed): ```bash - dotnet tool install --global dotnet-ef - dotnet add package Microsoft.EntityFrameworkCore.Design + dotnet tool install --global dotnet-ef --version 9.0.10 + dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.10 ``` 3. Add a migration to generate the DDL (if not already present): diff --git a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/Schema.OneToOne.csproj b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/Schema.OneToOne.csproj index 484e670b..f915940e 100644 --- a/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/Schema.OneToOne.csproj +++ b/examples/EntityFrameworkCore.Ydb.Samples/Schema.OneToOne/Schema.OneToOne.csproj @@ -13,7 +13,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index 4966623c..3eca7298 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,6 @@ +- Fixed bug: Missing/removed __EFMigrationsHistory caused INSERT errors (LockMigration); migrations now handle table recreation and locking properly ([#544](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/544)). +- Added support for new YDB data types - `Date32`, `Datetime64`, `Timestamp64` and `Interval64`. +- Fixed bug: Missing `DEFAULT` clause generation in `YdbMigrationsSqlGenerator` (handles defaultValue/defaultValueSql) ([#552](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/552)). - 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..da4c5785 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..32a38ddd --- /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/LazyLoadProxyYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/LazyLoadProxyYdbTest.cs index 221c6d57..07b3057d 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/LazyLoadProxyYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/LazyLoadProxyYdbTest.cs @@ -7,12 +7,9 @@ namespace EntityFrameworkCore.Ydb.FunctionalTests; -public class LazyLoadProxyYdbTest : LazyLoadProxyTestBase +public class LazyLoadProxyYdbTest(LazyLoadProxyYdbTest.LoadYdbFixture fixture) + : LazyLoadProxyTestBase(fixture) { - public LazyLoadProxyYdbTest(LoadYdbFixture fixture) : base(fixture) - { - } - [ConditionalFact(Skip = "TODO: Fix precision")] public override void Can_serialize_proxies_to_JSON() => base.Can_serialize_proxies_to_JSON(); @@ -32,5 +29,18 @@ public TestSqlLoggerFactory TestSqlLoggerFactory protected override ITestStoreFactory TestStoreFactory => YdbTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity() + .Property(e => e.Birthday) + .HasColumnType("Timestamp64"); + + modelBuilder.Entity() + .Property(e => e.Birthday) + .HasColumnType("Timestamp64"); + } } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/GearsOfWarQueryYdbFixture.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/GearsOfWarQueryYdbFixture.cs index 0dddba35..7643fe1a 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/GearsOfWarQueryYdbFixture.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/GearsOfWarQueryYdbFixture.cs @@ -20,14 +20,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity() .Property(e => e.Date) - .HasConversion( - v => v.ToDateTime(TimeOnly.MinValue), - v => DateOnly.FromDateTime(v)); + .HasColumnType("Date32"); modelBuilder.Entity() - .Property(e => e.Duration) + .Property(e => e.Time) + .HasColumnType("Datetime64") .HasConversion( - v => new DateTime(1970, 1, 1).Add(v), - v => v.TimeOfDay); + v => new DateTime(2000, 1, 1).Add(v.ToTimeSpan()), // Time → DateTime + v => new TimeOnly(v.TimeOfDay.Ticks) + ); // DateTime → Time + + modelBuilder.Entity() + .Property(e => e.IssueDate) + .HasColumnType("Date32"); } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPCGearsOfWarQueryYdbFixture.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPCGearsOfWarQueryYdbFixture.cs index c443d65c..076710e0 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPCGearsOfWarQueryYdbFixture.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPCGearsOfWarQueryYdbFixture.cs @@ -15,18 +15,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); + modelBuilder.Entity() + .Property(e => e.Date) + .HasColumnType("Date32"); + modelBuilder.Entity() .Property(e => e.Time) + .HasColumnType("Datetime64") .HasConversion( v => new DateTime(2000, 1, 1).Add(v.ToTimeSpan()), // Time → DateTime - v => new TimeOnly(v.TimeOfDay.Ticks)) // DateTime → Time - .HasColumnType("DATETIME"); + v => new TimeOnly(v.TimeOfDay.Ticks) + ); // DateTime → Time - modelBuilder.Entity() - .Property(e => e.Duration) - .HasConversion( - v => new DateTime(2000, 1, 1).Add(v), // TimeSpan → DateTime - v => v.TimeOfDay) // DateTime → TimeSpan - .HasColumnType("DATETIME"); + modelBuilder.Entity() + .Property(e => e.IssueDate) + .HasColumnType("Date32"); } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbFixture.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbFixture.cs index ce97a173..27d6c319 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbFixture.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbFixture.cs @@ -6,7 +6,7 @@ namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; -public class TptGearsOfWarQueryYdbFixture : TPTGearsOfWarQueryRelationalFixture +public class TPTGearsOfWarQueryYdbFixture : TPTGearsOfWarQueryRelationalFixture { protected override ITestStoreFactory TestStoreFactory => YdbTestStoreFactory.Instance; @@ -15,18 +15,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); + modelBuilder.Entity() + .Property(e => e.Date) + .HasColumnType("Date32"); + modelBuilder.Entity() .Property(e => e.Time) + .HasColumnType("Datetime64") .HasConversion( v => new DateTime(2000, 1, 1).Add(v.ToTimeSpan()), // Time → DateTime - v => new TimeOnly(v.TimeOfDay.Ticks)) // DateTime → Time - .HasColumnType("DATETIME"); + v => new TimeOnly(v.TimeOfDay.Ticks) + ); // DateTime → Time - modelBuilder.Entity() - .Property(e => e.Date) - .HasConversion( - v => new DateTime(v.Year, v.Month, v.Day), // DateOnly → DateTime - v => new DateOnly(v.Year, v.Month, v.Day)) // DateTime → DateOnly - .HasColumnType("DATETIME"); + modelBuilder.Entity() + .Property(e => e.IssueDate) + .HasColumnType("Date32"); } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbTest.cs index 74b71e79..b66e3533 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/TPTGearsOfWarQueryYdbTest.cs @@ -5,10 +5,10 @@ namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; // TODO: Correlated subqueries right now are not supported in YDB -public class TptGearsOfWarQueryYdbTest : TPTGearsOfWarQueryRelationalTestBase +public class TPTGearsOfWarQueryYdbTest : TPTGearsOfWarQueryRelationalTestBase { // ReSharper disable once UnusedParameter.Local - public TptGearsOfWarQueryYdbTest(TptGearsOfWarQueryYdbFixture fixture, ITestOutputHelper testOutputHelper) + public TPTGearsOfWarQueryYdbTest(TPTGearsOfWarQueryYdbFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); 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..e3b1a4eb 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,6 @@ +- Feat ADO.NET: Added overflow-checked int-to-uint cast for YDB `Date`, `Datetime`, and `Timestamp`. `DateTime` before epoch now throws `OverflowException`. +- 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/Internal/YdbValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs index 9ad5c1b5..ac305616 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs @@ -143,7 +143,7 @@ internal static Guid UnpackUuid(this Ydb.Value value) } internal static Ydb.Value PackDate(DateTime value) => - new() { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }; + new() { Uint32Value = checked((uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay)) }; internal static DateTime UnpackDate(this Ydb.Value value) => UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerDay); @@ -155,7 +155,7 @@ internal static DateTime UnpackDate32(this Ydb.Value value) => UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay); internal static Ydb.Value PackDatetime(DateTime value) => new() - { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond) }; + { Uint32Value = checked((uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond)) }; internal static DateTime UnpackDatetime(this Ydb.Value value) => UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerSecond); @@ -167,7 +167,7 @@ internal static DateTime UnpackDatetime64(this Ydb.Value value) => UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond); internal static Ydb.Value PackTimestamp(DateTime value) => new() - { Uint64Value = (ulong)(value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond }; + { Uint64Value = checked((ulong)(value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond) }; internal static DateTime UnpackTimestamp(this Ydb.Value value) => UnixEpoch.AddTicks((long)(value.Uint64Value * TimeSpanUtils.TicksPerMicrosecond)); 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..73b28687 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -57,6 +57,14 @@ public void YdbParameter_WhenNoSupportedDbType_ThrowException(DbType dbType, str Assert.Equal("Ydb don't supported this DbType: " + name, Assert.Throws(() => new YdbParameter("$parameter", dbType) { IsNullable = true }.TypedValue).Message); + [Theory] + [InlineData(YdbDbType.Date)] + [InlineData(YdbDbType.Datetime)] + [InlineData(YdbDbType.Timestamp)] + public void YdbParameter_WhenDateTimeBeforeEpoch_ForDateDatetimeTimestamp_ThrowsOverflowException( + YdbDbType ydbDbType) => Assert.Throws(() => new YdbParameter("$parameter", ydbDbType) + { Value = new DateTime(1950, 1, 1) }.TypedValue); + [Fact] public void YdbParameter_WhenSetAndNoSet_ReturnValueOrException() { @@ -140,10 +148,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]