diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e6db48a..a642bc7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,7 +54,6 @@ jobs: - 8765:8765 env: YDB_LOCAL_SURVIVE_RESTART: true - YDB_USE_IN_MEMORY_PDISKS: true options: '--name ydb-local -h localhost' env: OS: ubuntu-22.04 @@ -72,7 +71,7 @@ jobs: run: | docker cp ydb-local:/ydb_certs/ca.pem ~/ cd src - dotnet test --filter "(FullyQualifiedName~Ado) | (FullyQualifiedName~Dapper)" -l "console;verbosity=normal" + dotnet test --filter "(FullyQualifiedName~Ado) | (FullyQualifiedName~Dapper)" -f ${{ matrix.dotnet-target-framework }} -l "console;verbosity=normal" topic-tests: runs-on: ubuntu-22.04 strategy: @@ -94,7 +93,6 @@ jobs: - 8765:8765 env: YDB_LOCAL_SURVIVE_RESTART: true - YDB_USE_IN_MEMORY_PDISKS: true options: '--name ydb-local -h localhost' env: OS: ubuntu-22.04 @@ -112,7 +110,7 @@ jobs: run: | docker cp ydb-local:/ydb_certs/ca.pem ~/ cd src - dotnet test --filter "FullyQualifiedName~Topic" -l "console;verbosity=normal" + dotnet test --filter "FullyQualifiedName~Topic" -f ${{ matrix.dotnet-target-framework }} -l "console;verbosity=normal" integration-tests: runs-on: ubuntu-22.04 strategy: @@ -134,7 +132,6 @@ jobs: - 8765:8765 env: YDB_LOCAL_SURVIVE_RESTART: true - YDB_USE_IN_MEMORY_PDISKS: true options: '--name ydb-local -h localhost' env: OS: ubuntu-22.04 diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 406beb1f..664cab6c 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -8,7 +8,10 @@ internal static class PoolManager private static readonly SemaphoreSlim SemaphoreSlim = new(1); // async mutex private static readonly ConcurrentDictionary Pools = new(); - internal static async Task GetSession(YdbConnectionStringBuilder connectionString) + internal static async Task GetSession( + YdbConnectionStringBuilder connectionString, + CancellationToken cancellationToken + ) { if (Pools.TryGetValue(connectionString.ConnectionString, out var sessionPool)) { @@ -17,7 +20,7 @@ internal static async Task GetSession(YdbConnectionStringBuilder connec try { - await SemaphoreSlim.WaitAsync(); + await SemaphoreSlim.WaitAsync(cancellationToken); if (Pools.TryGetValue(connectionString.ConnectionString, out var pool)) { diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 2242435e..d6f40598 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -1,5 +1,6 @@ using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Text; using Ydb.Sdk.Ado.Internal; using Ydb.Sdk.Services.Query; @@ -8,9 +9,13 @@ namespace Ydb.Sdk.Ado; public sealed class YdbCommand : DbCommand { - private YdbConnection YdbConnection { get; set; } + private YdbConnection? YdbConnection { get; set; } - private string _commandText = string.Empty; + private string? _commandText = string.Empty; + + public YdbCommand() + { + } public YdbCommand(YdbConnection ydbConnection) { @@ -65,14 +70,10 @@ public override void Prepare() public override string CommandText { - get => _commandText; + get => _commandText ?? throw new InvalidOperationException("CommandText property has not been initialized"); #pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - set + [param: AllowNull] set => _commandText = value; #pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - { - _commandText = value ?? throw new ArgumentNullException(nameof(value)); - DbParameterCollection.Clear(); - } } public override int CommandTimeout @@ -99,14 +100,14 @@ protected override DbConnection? DbConnection get => YdbConnection; set { - if (value is YdbConnection ydbConnection) + if (value is null or Ado.YdbConnection) { - YdbConnection = ydbConnection; + YdbConnection = (YdbConnection?)value; } else { throw new ArgumentException( - $"Unsupported DbTransaction type: {value?.GetType()}, expected: {typeof(YdbConnection)}"); + $"Unsupported DbTransaction type: {value.GetType()}, expected: {typeof(YdbConnection)}"); } } } @@ -154,7 +155,8 @@ protected override YdbDataReader ExecuteDbDataReader(CommandBehavior behavior) protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { - if (YdbConnection.IsBusy) + if (YdbConnection?.IsBusy + ?? throw new InvalidOperationException("Connection property has not been initialized.")) { throw new YdbOperationInProgressException(YdbConnection); } diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index dc0e9826..195e9343 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -9,6 +9,12 @@ public sealed class YdbConnection : DbConnection { private static readonly YdbConnectionStringBuilder DefaultSettings = new(); + private static readonly StateChangeEventArgs ClosedToOpenEventArgs = + new(ConnectionState.Closed, ConnectionState.Open); + + private static readonly StateChangeEventArgs OpenToClosedEventArgs = + new(ConnectionState.Open, ConnectionState.Closed); + private bool _disposed; private YdbConnectionStringBuilder ConnectionStringBuilder { get; set; } @@ -84,7 +90,7 @@ public override async Task OpenAsync(CancellationToken cancellationToken) try { - Session = await PoolManager.GetSession(ConnectionStringBuilder); + Session = await PoolManager.GetSession(ConnectionStringBuilder, cancellationToken); } catch (Exception e) { @@ -92,10 +98,12 @@ public override async Task OpenAsync(CancellationToken cancellationToken) { Driver.TransportException transportException => new YdbException(transportException.Status), StatusUnsuccessfulException unsuccessfulException => new YdbException(unsuccessfulException.Status), - _ => new YdbException("Cannot get session", e) + _ => e }; } + OnStateChange(ClosedToOpenEventArgs); + ConnectionState = ConnectionState.Open; } @@ -118,6 +126,8 @@ public override async Task CloseAsync() await LastTransaction.RollbackAsync(); } + OnStateChange(OpenToClosedEventArgs); + ConnectionState = ConnectionState.Closed; } finally @@ -140,7 +150,9 @@ public override string ConnectionString } } - public override string Database => ConnectionStringBuilder.Database; + public override string Database => State == ConnectionState.Closed + ? string.Empty + : ConnectionStringBuilder.Database; public override ConnectionState State => ConnectionState; @@ -152,7 +164,16 @@ public override string ConnectionString internal bool IsBusy => LastReader is { IsClosed: false }; public override string DataSource => string.Empty; // TODO - public override string ServerVersion => string.Empty; // TODO + + public override string ServerVersion + { + get + { + EnsureConnectionOpen(); + + return string.Empty; // TODO ServerVersion + } + } protected override YdbCommand CreateDbCommand() { @@ -239,6 +260,11 @@ public override async ValueTask DisposeAsync() _disposed = true; } + /// + /// DB provider factory. + /// + protected override DbProviderFactory DbProviderFactory => YdbProviderFactory.Instance; + /// /// Clears the connection pool. All idle physical connections in the pool of the given connection are /// immediately closed, and any busy connections which were opened before was called diff --git a/src/Ydb.Sdk/src/Ado/YdbProviderFactory.cs b/src/Ydb.Sdk/src/Ado/YdbProviderFactory.cs new file mode 100644 index 00000000..120a0d9d --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/YdbProviderFactory.cs @@ -0,0 +1,35 @@ +using System.Data.Common; + +namespace Ydb.Sdk.Ado; + +public class YdbProviderFactory : DbProviderFactory +{ + public static readonly YdbProviderFactory Instance = new(); + + public override YdbCommand CreateCommand() + { + return new YdbCommand(); + } + + public override YdbConnection CreateConnection() + { + return new YdbConnection(); + } + + public override YdbConnectionStringBuilder CreateConnectionStringBuilder() + { + return new YdbConnectionStringBuilder(); + } + + public override DbParameter CreateParameter() + { + return new YdbParameter(); + } + +#if NET7_0_OR_GREATER + public override YdbDataSource CreateDataSource(string connectionString) + { + return new YdbDataSource(); + } +#endif +} diff --git a/src/Ydb.Sdk/tests/Ado/Specification/YdbConnectionTests.cs b/src/Ydb.Sdk/tests/Ado/Specification/YdbConnectionTests.cs new file mode 100644 index 00000000..5fe7210b --- /dev/null +++ b/src/Ydb.Sdk/tests/Ado/Specification/YdbConnectionTests.cs @@ -0,0 +1,51 @@ +using AdoNet.Specification.Tests; +using Xunit; + +namespace Ydb.Sdk.Tests.Ado.Specification; + +public class YdbConnectionTests : ConnectionTestBase +{ + public YdbConnectionTests(YdbFactoryFixture fixture) : base(fixture) + { + } + +#pragma warning disable xUnit1004 + [Fact(Skip = "IComponent legacy.")] +#pragma warning restore xUnit1004 + public override void Dispose_raises_Disposed() + { + base.Dispose_raises_Disposed(); + } + +#pragma warning disable xUnit1004 + [Fact(Skip = "IComponent legacy.")] +#pragma warning restore xUnit1004 + public override Task DisposeAsync_raises_Disposed() + { + return base.DisposeAsync_raises_Disposed(); + } + +#pragma warning disable xUnit1004 + [Fact(Skip = "Connect to default settings 'grpc://localhost:2136/local'.")] +#pragma warning restore xUnit1004 + public override void Open_throws_when_no_connection_string() + { + base.Open_throws_when_no_connection_string(); + } + +#pragma warning disable xUnit1004 + [Fact(Skip = "TODO Supported this field.")] +#pragma warning restore xUnit1004 + public override void ServerVersion_returns_value() + { + base.ServerVersion_returns_value(); + } + +#pragma warning disable xUnit1004 + [Fact(Skip = "TODO Supported cancel OpenAsync.")] +#pragma warning restore xUnit1004 + public override Task OpenAsync_is_canceled() + { + return base.OpenAsync_is_canceled(); + } +} diff --git a/src/Ydb.Sdk/tests/Ado/Specification/YdbFactoryFixture.cs b/src/Ydb.Sdk/tests/Ado/Specification/YdbFactoryFixture.cs new file mode 100644 index 00000000..6b586788 --- /dev/null +++ b/src/Ydb.Sdk/tests/Ado/Specification/YdbFactoryFixture.cs @@ -0,0 +1,12 @@ +using System.Data.Common; +using AdoNet.Specification.Tests; +using Ydb.Sdk.Ado; + +namespace Ydb.Sdk.Tests.Ado.Specification; + +public class YdbFactoryFixture : IDbFactoryFixture +{ + public DbProviderFactory Factory => YdbProviderFactory.Instance; + + public string ConnectionString => "Host=localhost;Port=2136;Database=local"; +} diff --git a/src/Ydb.Sdk/tests/Ado/YdbDataReaderTests.cs b/src/Ydb.Sdk/tests/Ado/YdbDataReaderTests.cs index 644b385e..586ebe2a 100644 --- a/src/Ydb.Sdk/tests/Ado/YdbDataReaderTests.cs +++ b/src/Ydb.Sdk/tests/Ado/YdbDataReaderTests.cs @@ -40,11 +40,11 @@ public async Task BasedIteration_WhenNotCallMethodRead_ThrowException() } [Fact] - public void CreateYdbDataReader_WhenAbortedStatus_ThrowException() + public async Task CreateYdbDataReader_WhenAbortedStatus_ThrowException() { var statuses = new List(); - Assert.Equal("Status: Aborted", Assert.Throws( - () => YdbDataReader.CreateYdbDataReader(SingleEnumeratorFailed, statuses.Add).GetAwaiter().GetResult()) + Assert.Equal("Status: Aborted", (await Assert.ThrowsAsync( + () => YdbDataReader.CreateYdbDataReader(SingleEnumeratorFailed, statuses.Add))) .Message); Assert.Single(statuses); Assert.Equal(StatusCode.Aborted, statuses[0].StatusCode); diff --git a/src/Ydb.Sdk/tests/Pool/ChannelPoolTests.cs b/src/Ydb.Sdk/tests/Pool/ChannelPoolTests.cs index efd5d385..a51cdf61 100644 --- a/src/Ydb.Sdk/tests/Pool/ChannelPoolTests.cs +++ b/src/Ydb.Sdk/tests/Pool/ChannelPoolTests.cs @@ -96,8 +96,7 @@ public async Task GetChannel_WhenRaceCondition_ChannelIsCreatedOneTime(bool useA .Select(endpoint => Task.Run(() => _channelPool.GetChannel(endpoint))) .ToArray(); - // ReSharper disable once CoVariantArrayConversion - Task.WaitAll(tasks); + await Task.WhenAll(tasks); _mockChannelFactory.Verify( channelPool => channelPool.CreateChannel(It.IsAny()), Times.Exactly(endpointCount) diff --git a/src/Ydb.Sdk/tests/Tests.csproj b/src/Ydb.Sdk/tests/Tests.csproj index 18e84cda..78a99fea 100644 --- a/src/Ydb.Sdk/tests/Tests.csproj +++ b/src/Ydb.Sdk/tests/Tests.csproj @@ -16,12 +16,13 @@ + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ydb.Sdk/tests/Value/BasicUnitTests.cs b/src/Ydb.Sdk/tests/Value/BasicUnitTests.cs index 9022bdff..8d2a5c61 100644 --- a/src/Ydb.Sdk/tests/Value/BasicUnitTests.cs +++ b/src/Ydb.Sdk/tests/Value/BasicUnitTests.cs @@ -327,7 +327,7 @@ public void ListType() var elements = value.GetTuple(); Assert.Equal(2, elements.Count); - Assert.Equal(0, elements[0].GetList().Count); + Assert.Empty(elements[0].GetList()); Assert.Equal(new[] { "one", "two" }, elements[1].GetList().Select(v => (string)v!)); } @@ -340,9 +340,9 @@ public void TupleType() }); var elements = value.GetTuple(); - Assert.Equal(1, elements.Count); + Assert.Single(elements); - Assert.Equal(0, elements[0].GetTuple().Count); + Assert.Empty(elements[0].GetTuple()); } [Fact] @@ -361,7 +361,7 @@ public void StructType() var elements = value.GetTuple(); Assert.Equal(2, elements.Count); - Assert.Equal(0, elements[0].GetStruct().Count); + Assert.Empty(elements[0].GetStruct()); var s = elements[1].GetStruct(); Assert.Equal(2, s.Count);