Skip to content

Commit 947664c

Browse files
authored
Always use savepoints when nesting transactions (#1135)
1 parent cb65dbc commit 947664c

File tree

5 files changed

+57
-5
lines changed

5 files changed

+57
-5
lines changed

src/Model.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -1640,16 +1640,16 @@ public function insert(array $row)
16401640
{
16411641
$entity = $this->createEntity();
16421642

1643-
$hasArrayValue = false;
1643+
$hasRefs = false;
16441644
foreach ($row as $v) {
16451645
if (is_array($v)) {
1646-
$hasArrayValue = true;
1646+
$hasRefs = true;
16471647

16481648
break;
16491649
}
16501650
}
16511651

1652-
if (!$hasArrayValue) {
1652+
if (!$hasRefs) {
16531653
$entity->_insert($row);
16541654
} else {
16551655
$this->atomic(static function () use ($entity, $row) {

src/Persistence/Sql/Connection.php

+2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ public static function connect($dsn, $user = null, $password = null, $defaults =
201201
$dbalConnection = $connectionClass::connectFromDbalDriverConnection($dbalDriverConnection);
202202
}
203203

204+
$dbalConnection->setNestTransactionsWithSavepoints(true); // remove once DBAL 3.x support is dropped
205+
204206
$connection = new $connectionClass($defaults);
205207
$connection->_connection = $dbalConnection;
206208

src/Schema/TestSqlPersistence.php

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public function getConnection(): Persistence\Sql\Connection
3636
new class() implements SQLLogger {
3737
public function startQuery($sql, array $params = null, array $types = null): void
3838
{
39+
// log transaction savepoint operations only once
40+
// https://github.com/doctrine/dbal/blob/3.6.7/src/Connection.php#L1365
41+
if (preg_match('~^(?:SAVEPOINT|RELEASE SAVEPOINT|ROLLBACK TO SAVEPOINT|SAVE TRANSACTION|ROLLBACK TRANSACTION) DOCTRINE2_SAVEPOINT_\d+;?$~', $sql)) {
42+
return;
43+
}
44+
3945
// fix https://github.com/doctrine/dbal/issues/5525
4046
if ($params !== null && $params !== [] && array_is_list($params)) {
4147
$params = array_combine(range(1, count($params)), $params);

tests/Schema/TestCaseTest.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public function testLogQuery(): void
5656
"START TRANSACTION";
5757
5858
59+
"SAVEPOINT";
60+
61+
5962
insert into `t` (`name`, `int`, `float`, `null`)
6063
values
6164
(
@@ -86,9 +89,9 @@ public function testLogQuery(): void
8689
and `id` = 1
8790
EOF
8891
. $makeLimitSqlFx(2)
92+
. ";\n\n"
93+
. ($this->getDatabasePlatform()->supportsReleaseSavepoints() ? "\n\"RELEASE SAVEPOINT\";\n\n" : '')
8994
. <<<'EOF'
90-
;
91-
9295
9396
"COMMIT";
9497

tests/TransactionTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ public function testAtomicInMiddleOfResultIteration(): void
8282
], $this->getDb()['item']);
8383
}
8484

85+
public function testAtomicWithRollbackToSavepoint(): void
86+
{
87+
$this->setDb([
88+
'item' => [
89+
['name' => 'John'],
90+
['name' => 'Sue'],
91+
['name' => 'Smith'],
92+
],
93+
]);
94+
95+
$m = new Model($this->db, ['table' => 'item']);
96+
$m->addField('name');
97+
$m->setOrder('id');
98+
99+
$this->db->atomic(function () use ($m) {
100+
foreach ($m as $entity) {
101+
$rollback = $entity->get('name') === 'Sue';
102+
103+
try {
104+
$this->db->atomic(static function () use ($entity, $rollback) {
105+
$entity->set('name', $entity->get('name') . ' 2');
106+
$entity->save();
107+
108+
if ($rollback) {
109+
throw new \Exception('Rollback to savepoint');
110+
}
111+
});
112+
} catch (\Exception $e) {
113+
self::assertSame('Rollback to savepoint', $e->getMessage());
114+
self::assertTrue($rollback);
115+
}
116+
}
117+
});
118+
119+
self::assertSame([
120+
1 => ['id' => 1, 'name' => 'John 2'],
121+
['id' => 2, 'name' => 'Sue'],
122+
['id' => 3, 'name' => 'Smith 2'],
123+
], $this->getDb()['item']);
124+
}
125+
85126
public function testBeforeSaveHook(): void
86127
{
87128
$this->setDb([

0 commit comments

Comments
 (0)