Skip to content

Commit 0d8a3d1

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

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

test/transaction_isolation_test.dart

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
// If we complete both transactions in parallel, we get an unexpected
102+
// exception
103+
// c2.complete();
104+
105+
await session
106+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
107+
// If we complete the first transaction after the second transaction
108+
// the correct exception is thrown
109+
c2.complete();
110+
},
111+
),
112+
);
113+
114+
expectLater(
115+
f1,
116+
throwsA(
117+
isA<ServerException>()
118+
.having((e) => e.severity, 'Exception severity', Severity.error)
119+
.having((e) => e.code, 'Exception code', '40001'),
120+
),
121+
);
122+
await f2;
123+
});
124+
125+
test(
126+
'when two transactions using repeatable read isolation level'
127+
'reads the row updated by the other transaction'
128+
'then one transaction throws exception ', () async {
129+
final c1 = Completer();
130+
final c2 = Completer();
131+
final f1 = Future.microtask(
132+
() => conn1.runTx(
133+
settings: TransactionSettings(
134+
isolationLevel: IsolationLevel.serializable,
135+
),
136+
(session) async {
137+
await session.execute('SELECT * from t WHERE id=1');
138+
139+
c1.complete();
140+
await c2.future;
141+
142+
await session
143+
.execute('UPDATE t SET counter = counter + 10 WHERE id=2');
144+
},
145+
),
146+
);
147+
final f2 = Future.microtask(
148+
() => conn2.runTx(
149+
settings: TransactionSettings(
150+
isolationLevel: IsolationLevel.serializable,
151+
),
152+
(session) async {
153+
await session.execute('SELECT * from t WHERE id=2');
154+
155+
await c1.future;
156+
// If we complete both transactions in parallel, we get an unexpected
157+
// exception
158+
c2.complete();
159+
160+
await session
161+
.execute('UPDATE t SET counter = counter + 20 WHERE id=1');
162+
// If we complete the first transaction after the second transaction
163+
// the correct exception is thrown
164+
// c2.complete();
165+
},
166+
),
167+
);
168+
169+
// This test throws Severity.error Session or transaction has already
170+
// finished, did you forget to await a statement?
171+
await expectLater(
172+
() => Future.wait([f1, f2]),
173+
throwsA(
174+
isA<ServerException>()
175+
.having((e) => e.severity, 'Exception severity', Severity.error)
176+
.having((e) => e.code, 'Exception code', '40001'),
177+
),
178+
);
179+
});
180+
});
181+
});
182+
}

0 commit comments

Comments
 (0)