Skip to content

Commit cc6f10a

Browse files
committed
test: add test for serializable transaction isolation
1 parent 03d4221 commit cc6f10a

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

test/transaction_isolation_test.dart

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// ignore_for_file: unawaited_futures
2+
import 'dart:async';
3+
4+
import 'package:postgres/postgres.dart';
5+
import 'package:test/test.dart';
6+
7+
import 'docker.dart';
8+
9+
void main() {
10+
withPostgresServer('Transaction isolation level', (server) {
11+
group('Given two rows in the database and two database connections', () {
12+
late Connection conn1;
13+
late Connection conn2;
14+
setUp(() async {
15+
conn1 = await server.newConnection();
16+
conn2 = await server.newConnection();
17+
await conn1.execute('CREATE TABLE t (id INT PRIMARY KEY, counter INT)');
18+
await conn1.execute('INSERT INTO t VALUES (1, 0)');
19+
await conn1.execute('INSERT INTO t VALUES (2, 1)');
20+
});
21+
22+
tearDown(() async {
23+
await conn1.execute('DROP TABLE t;');
24+
await conn1.close();
25+
await conn2.close();
26+
});
27+
28+
test(
29+
'when two transactions using repeatable read isolation level'
30+
'reads the row updated by the other transaction'
31+
'then rows are updated', () async {
32+
final c1 = Completer();
33+
final c2 = Completer();
34+
final f1 = Future.microtask(() => conn1.runTx(
35+
settings: TransactionSettings(
36+
isolationLevel: IsolationLevel.repeatableRead,
37+
),
38+
(session) async {
39+
await session.execute('SELECT * from t WHERE id=1');
40+
41+
c1.complete();
42+
await c2.future;
43+
44+
await session
45+
.execute('UPDATE t SET counter = counter + 10 WHERE id=2');
46+
},
47+
));
48+
final f2 = Future.microtask(() => conn2.runTx(
49+
settings: TransactionSettings(
50+
isolationLevel: IsolationLevel.repeatableRead,
51+
),
52+
(session) async {
53+
await session.execute('SELECT * from t WHERE id=2');
54+
55+
await c1.future;
56+
c2.complete();
57+
58+
await session
59+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
60+
},
61+
));
62+
await Future.wait([f1, f2]);
63+
final rs = await conn1.execute('SELECT * from t WHERE id=1');
64+
expect(rs.single, [1, 20]);
65+
final rs2 = await conn1.execute('SELECT * from t WHERE id=2');
66+
expect(rs2.single, [2, 11]);
67+
});
68+
69+
test(
70+
'when two transactions using repeatable read isolation level'
71+
'reads the row updated by the other transaction'
72+
'then one transaction throws exception ', () async {
73+
// This test works as expected and a ServerException with
74+
final c1 = Completer();
75+
final c2 = Completer();
76+
final f1 = Future.microtask(
77+
() => conn1.runTx(
78+
settings: TransactionSettings(
79+
isolationLevel: IsolationLevel.serializable,
80+
),
81+
(session) async {
82+
await session.execute('SELECT * from t WHERE id=1');
83+
84+
c1.complete();
85+
await c2.future;
86+
87+
await session
88+
.execute('UPDATE t SET counter = counter + 10 WHERE id=2');
89+
},
90+
),
91+
);
92+
final f2 = Future.microtask(
93+
() => conn2.runTx(
94+
settings: TransactionSettings(
95+
isolationLevel: IsolationLevel.serializable,
96+
),
97+
(session) async {
98+
await session.execute('SELECT * from t WHERE id=2');
99+
100+
await c1.future;
101+
102+
await session
103+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
104+
105+
// If we complete the first transaction after the second transaction
106+
// then the first transaction throws the correct exception
107+
c2.complete();
108+
},
109+
),
110+
);
111+
112+
expectLater(
113+
f1,
114+
throwsA(
115+
isA<ServerException>()
116+
.having((e) => e.severity, 'Exception severity', Severity.error)
117+
.having((e) => e.code, 'Exception code', '40001'),
118+
),
119+
);
120+
await f2;
121+
});
122+
123+
test(
124+
'when two transactions using repeatable read isolation level'
125+
'reads the row updated by the other transaction'
126+
'then one transaction throws exception ', () async {
127+
final c1 = Completer();
128+
final c2 = Completer();
129+
final f1 = Future.microtask(
130+
() => conn1.runTx(
131+
settings: TransactionSettings(
132+
isolationLevel: IsolationLevel.serializable,
133+
),
134+
(session) async {
135+
await session.execute('SELECT * from t WHERE id=1');
136+
137+
c1.complete();
138+
await c2.future;
139+
140+
await session
141+
.execute('UPDATE t SET counter = counter + 10 WHERE id=2');
142+
},
143+
),
144+
);
145+
final f2 = Future.microtask(
146+
() => conn2.runTx(
147+
settings: TransactionSettings(
148+
isolationLevel: IsolationLevel.serializable,
149+
),
150+
(session) async {
151+
await session.execute('SELECT * from t WHERE id=2');
152+
153+
await c1.future;
154+
// If we complete both transactions in parallel then we don't get
155+
// the correct exception
156+
c2.complete();
157+
158+
await session
159+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
160+
},
161+
),
162+
);
163+
164+
// This test throws Severity.error Session or transaction has already
165+
// finished, did you forget to await a statement?
166+
await expectLater(
167+
() => Future.wait([f1, f2]),
168+
throwsA(
169+
isA<ServerException>()
170+
.having((e) => e.severity, 'Exception severity', Severity.error)
171+
.having((e) => e.code, 'Exception code', '40001'),
172+
),
173+
);
174+
});
175+
});
176+
});
177+
}

0 commit comments

Comments
 (0)