Skip to content

Commit 595839b

Browse files
dev: Added specification tests for YdbTransaction (#257)
* InvalidOperationException on ConnectionString property has not been initialized. * One YdbTransaction per YdbConnection. Exception: InvalidOperationException("A transaction is already in progress; nested/concurrent transactions aren't supported."); * ConnectionString returns an empty.String when it is not set * When a YdbDataReader is closed, if stream is not empty, a YdbTransaction fails if it is not null. A session also fails due to a possible error SessionBusy race condition with the server. * FIX BUG: Fetch txId from the last result set. * YdbTransaction CheckDisposed() (invoke rollback if transaction hasn't been committed) * Added specification tests for YdbTransaction
1 parent 23e4818 commit 595839b

18 files changed

+354
-228
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
strategy:
3939
fail-fast: false
4040
matrix:
41-
ydb-version: [ 'trunk' ]
41+
ydb-version: [ 'trunk', 'latest' ]
4242
dotnet-version: [ 6.0.x, 7.0.x ]
4343
include:
4444
- dotnet-version: 6.0.x

src/Ydb.Sdk/src/Ado/YdbCommand.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,14 @@ protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBeha
185185
? new ExecuteQuerySettings { TransportTimeout = TimeSpan.FromSeconds(CommandTimeout) }
186186
: new ExecuteQuerySettings();
187187

188+
var transaction = YdbConnection.CurrentTransaction;
189+
188190
var ydbDataReader = await YdbDataReader.CreateYdbDataReader(YdbConnection.Session.ExecuteQuery(
189-
preparedSql.ToString(), ydbParameters, execSettings, Transaction?.TransactionControl),
190-
YdbConnection.Session.OnStatus, Transaction);
191+
preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl),
192+
YdbConnection.Session.OnStatus, transaction);
191193

192194
YdbConnection.LastReader = ydbDataReader;
193195
YdbConnection.LastCommand = CommandText;
194-
YdbConnection.LastTransaction = Transaction;
195196

196197
return ydbDataReader;
197198
}

src/Ydb.Sdk/src/Ado/YdbConnection.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
using System.Data;
22
using System.Data.Common;
3+
using System.Diagnostics.CodeAnalysis;
34
using Ydb.Sdk.Services.Query;
45
using static System.Data.IsolationLevel;
56

67
namespace Ydb.Sdk.Ado;
78

89
public sealed class YdbConnection : DbConnection
910
{
10-
private static readonly YdbConnectionStringBuilder DefaultSettings = new();
11-
1211
private static readonly StateChangeEventArgs ClosedToOpenEventArgs =
1312
new(ConnectionState.Closed, ConnectionState.Open);
1413

1514
private static readonly StateChangeEventArgs OpenToClosedEventArgs =
1615
new(ConnectionState.Open, ConnectionState.Closed);
1716

1817
private bool _disposed;
18+
private YdbConnectionStringBuilder? _connectionStringBuilder;
1919

20-
private YdbConnectionStringBuilder ConnectionStringBuilder { get; set; }
20+
private YdbConnectionStringBuilder ConnectionStringBuilder
21+
{
22+
get => _connectionStringBuilder ??
23+
throw new InvalidOperationException("The ConnectionString property has not been initialized.");
24+
[param: AllowNull] init => _connectionStringBuilder = value;
25+
}
2126

2227
internal Session Session
2328
{
@@ -34,7 +39,6 @@ internal Session Session
3439

3540
public YdbConnection()
3641
{
37-
ConnectionStringBuilder = DefaultSettings;
3842
}
3943

4044
public YdbConnection(string connectionString)
@@ -67,7 +71,16 @@ public YdbTransaction BeginTransaction(TxMode txMode = TxMode.SerializableRw)
6771
{
6872
EnsureConnectionOpen();
6973

70-
return new YdbTransaction(this, txMode);
74+
if (CurrentTransaction is { Completed: false })
75+
{
76+
throw new InvalidOperationException(
77+
"A transaction is already in progress; nested/concurrent transactions aren't supported."
78+
);
79+
}
80+
81+
CurrentTransaction = new YdbTransaction(this, txMode);
82+
83+
return CurrentTransaction;
7184
}
7285

7386
public override void ChangeDatabase(string databaseName)
@@ -121,9 +134,9 @@ public override async Task CloseAsync()
121134
await LastReader.CloseAsync();
122135
}
123136

124-
if (LastTransaction is { Completed: false })
137+
if (CurrentTransaction is { Completed: false })
125138
{
126-
await LastTransaction.RollbackAsync();
139+
await CurrentTransaction.RollbackAsync();
127140
}
128141

129142
OnStateChange(OpenToClosedEventArgs);
@@ -138,15 +151,15 @@ public override async Task CloseAsync()
138151

139152
public override string ConnectionString
140153
{
141-
get => ConnectionStringBuilder.ConnectionString;
154+
get => _connectionStringBuilder?.ConnectionString ?? string.Empty;
142155
#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
143156
set
144157
#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes).
145158
{
146159
EnsureConnectionClosed();
147160

148161
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
149-
ConnectionStringBuilder = value != null ? new YdbConnectionStringBuilder(value) : DefaultSettings;
162+
_connectionStringBuilder = value != null ? new YdbConnectionStringBuilder(value) : null;
150163
}
151164
}
152165

@@ -160,8 +173,8 @@ public override string ConnectionString
160173

161174
internal YdbDataReader? LastReader { get; set; }
162175
internal string LastCommand { get; set; } = string.Empty;
163-
internal YdbTransaction? LastTransaction { get; set; }
164176
internal bool IsBusy => LastReader is { IsClosed: false };
177+
internal YdbTransaction? CurrentTransaction { get; private set; }
165178

166179
public override string DataSource => string.Empty; // TODO
167180

src/Ydb.Sdk/src/Ado/YdbDataReader.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,22 @@ public override IEnumerator<YdbDataRecord> GetEnumerator()
434434

435435
public override async Task CloseAsync()
436436
{
437+
if (ReaderState == State.Closed)
438+
{
439+
return;
440+
}
441+
437442
ReaderState = State.Closed;
443+
_onNotSuccessStatus(new Status(StatusCode.SessionBusy));
438444

439445
await _stream.DisposeAsync();
446+
447+
if (_ydbTransaction != null)
448+
{
449+
_ydbTransaction.Failed = true;
450+
451+
throw new YdbException("YdbDataReader was closed during transaction execution. Transaction is broken!");
452+
}
440453
}
441454

442455
public override void Close()
@@ -497,7 +510,7 @@ private async Task<State> NextExecPart()
497510
_currentResultSet = part.ResultSet?.FromProto();
498511
ReaderMetadata = _currentResultSet != null ? new Metadata(_currentResultSet) : EmptyMetadata.Instance;
499512

500-
if (_ydbTransaction != null)
513+
if (_ydbTransaction != null && part.TxMeta != null)
501514
{
502515
_ydbTransaction.TxId ??= part.TxMeta.Id;
503516
}

src/Ydb.Sdk/src/Ado/YdbTransaction.cs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public sealed class YdbTransaction : DbTransaction
1010
private readonly TxMode _txMode;
1111

1212
private bool _failed;
13+
private YdbConnection? _ydbConnection;
14+
private bool _isDisposed;
1315

1416
internal string? TxId { get; set; }
1517
internal bool Completed { get; private set; }
@@ -32,7 +34,7 @@ internal bool Failed
3234

3335
internal YdbTransaction(YdbConnection ydbConnection, TxMode txMode)
3436
{
35-
DbConnection = ydbConnection;
37+
_ydbConnection = ydbConnection;
3638
_txMode = txMode;
3739
}
3840

@@ -44,7 +46,7 @@ public override void Commit()
4446
// TODO propagate cancellation token
4547
public override async Task CommitAsync(CancellationToken cancellationToken = new())
4648
{
47-
await FinishTransaction(txId => DbConnection.Session.CommitTransaction(txId));
49+
await FinishTransaction(txId => DbConnection!.Session.CommitTransaction(txId));
4850
}
4951

5052
public override void Rollback()
@@ -62,36 +64,43 @@ public override void Rollback()
6264
return;
6365
}
6466

65-
await FinishTransaction(txId => DbConnection.Session.RollbackTransaction(txId));
67+
await FinishTransaction(txId => DbConnection!.Session.RollbackTransaction(txId));
6668
}
6769

68-
protected override YdbConnection DbConnection { get; }
70+
protected override YdbConnection? DbConnection
71+
{
72+
get
73+
{
74+
CheckDisposed();
75+
return _ydbConnection;
76+
}
77+
}
6978

7079
public override IsolationLevel IsolationLevel => _txMode == TxMode.SerializableRw
7180
? IsolationLevel.Serializable
7281
: IsolationLevel.Unspecified;
7382

7483
private async Task FinishTransaction(Func<string, Task<Status>> finishMethod)
7584
{
76-
if (Completed || DbConnection.State == ConnectionState.Closed)
85+
if (DbConnection?.State == ConnectionState.Closed || Completed)
7786
{
7887
throw new InvalidOperationException("This YdbTransaction has completed; it is no longer usable");
7988
}
8089

81-
if (DbConnection.IsBusy)
90+
if (DbConnection!.IsBusy)
8291
{
8392
throw new YdbOperationInProgressException(DbConnection);
8493
}
8594

86-
Completed = true;
87-
88-
if (TxId == null)
89-
{
90-
return; // transaction isn't started
91-
}
92-
9395
try
9496
{
97+
Completed = true;
98+
99+
if (TxId == null)
100+
{
101+
return; // transaction isn't started
102+
}
103+
95104
var status = await finishMethod(TxId); // Commit or Rollback
96105

97106
if (status.IsNotSuccess)
@@ -111,5 +120,43 @@ private async Task FinishTransaction(Func<string, Task<Status>> finishMethod)
111120

112121
throw new YdbException(e.Status);
113122
}
123+
finally
124+
{
125+
_ydbConnection = null;
126+
}
127+
}
128+
129+
protected override void Dispose(bool disposing)
130+
{
131+
if (_isDisposed || !disposing)
132+
return;
133+
134+
if (!Completed)
135+
{
136+
Rollback();
137+
}
138+
139+
_isDisposed = true;
140+
}
141+
142+
public override async ValueTask DisposeAsync()
143+
{
144+
if (_isDisposed)
145+
return;
146+
147+
if (!Completed)
148+
{
149+
await RollbackAsync();
150+
}
151+
152+
_isDisposed = true;
153+
}
154+
155+
private void CheckDisposed()
156+
{
157+
if (_isDisposed)
158+
{
159+
throw new ObjectDisposedException(nameof(YdbTransaction));
160+
}
114161
}
115162
}

src/Ydb.Sdk/tests/Ado/Specification/YdbConnectionTests.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ public override Task DisposeAsync_raises_Disposed()
2525
return base.DisposeAsync_raises_Disposed();
2626
}
2727

28-
#pragma warning disable xUnit1004
29-
[Fact(Skip = "Connect to default settings 'grpc://localhost:2136/local'.")]
30-
#pragma warning restore xUnit1004
31-
public override void Open_throws_when_no_connection_string()
32-
{
33-
base.Open_throws_when_no_connection_string();
34-
}
35-
3628
#pragma warning disable xUnit1004
3729
[Fact(Skip = "TODO Supported this field.")]
3830
#pragma warning restore xUnit1004

src/Ydb.Sdk/tests/Ado/Specification/YdbFactoryFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ public class YdbFactoryFixture : IDbFactoryFixture
88
{
99
public DbProviderFactory Factory => YdbProviderFactory.Instance;
1010

11-
public string ConnectionString => "Host=localhost;Port=2136;Database=local";
11+
public string ConnectionString => "Host=localhost;Port=2136;Database=local;MaxSessionPool=10";
1212
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using AdoNet.Specification.Tests;
2+
3+
namespace Ydb.Sdk.Tests.Ado.Specification;
4+
5+
public class YdbTransactionTests : TransactionTestBase<YdbFactoryFixture>
6+
{
7+
public YdbTransactionTests(YdbFactoryFixture fixture) : base(fixture)
8+
{
9+
}
10+
}
Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
11
using Xunit;
22
using Ydb.Sdk.Ado;
3+
using Ydb.Sdk.Tests.Ado.Specification;
4+
using Ydb.Sdk.Tests.Fixture;
35

46
namespace Ydb.Sdk.Tests.Ado;
57

6-
public class YdbAdoUserPasswordTests
8+
public class YdbAdoUserPasswordTests : YdbAdoNetFixture
79
{
10+
public YdbAdoUserPasswordTests(YdbFactoryFixture fixture) : base(fixture)
11+
{
12+
}
13+
814
[Fact]
915
public async Task Authentication_WhenUserAndPassword_ReturnValidConnection()
1016
{
11-
await using var connection = new YdbConnection();
12-
await connection.OpenAsync();
13-
17+
await using var connection = await CreateOpenConnectionAsync();
1418
var ydbCommand = connection.CreateCommand();
1519
var kurdyukovkirya = "kurdyukovkirya" + Random.Shared.Next();
1620
ydbCommand.CommandText = $"CREATE USER {kurdyukovkirya} PASSWORD 'password'";
1721
await ydbCommand.ExecuteNonQueryAsync();
1822
await connection.CloseAsync();
1923

20-
await using var userPasswordConnection = new YdbConnection($"User={kurdyukovkirya};Password=password;");
24+
await using var userPasswordConnection =
25+
new YdbConnection($"{ConnectionString};User={kurdyukovkirya};Password=password;");
2126
await userPasswordConnection.OpenAsync();
2227
ydbCommand = userPasswordConnection.CreateCommand();
2328
ydbCommand.CommandText = "SELECT 1 + 2";
2429
Assert.Equal(3, await ydbCommand.ExecuteScalarAsync());
2530

26-
await using var newConnection = new YdbConnection();
27-
await newConnection.OpenAsync();
31+
await using var newConnection = await CreateOpenConnectionAsync();
2832
ydbCommand = newConnection.CreateCommand();
2933
ydbCommand.CommandText = $"DROP USER {kurdyukovkirya};";
3034
await ydbCommand.ExecuteNonQueryAsync();
3135
}
36+
37+
[Fact]
38+
public async Task ExecuteNonQueryAsync_WhenCreateUser_ReturnEmptyResultSet()
39+
{
40+
await using var connection = await CreateOpenConnectionAsync();
41+
var dbCommand = connection.CreateCommand();
42+
var user = "user" + Random.Shared.Next();
43+
dbCommand.CommandText = $"CREATE USER {user} PASSWORD '123qweqwe'";
44+
await dbCommand.ExecuteNonQueryAsync();
45+
dbCommand.CommandText = $"DROP USER {user};";
46+
await dbCommand.ExecuteNonQueryAsync();
47+
}
3248
}

0 commit comments

Comments
 (0)