Thanks to visit codestin.com
Credit goes to github.com

Skip to content

PHP 8.0 PDO::rollback() now throws PDOException after implicit commit #35380

Closed
@stemis

Description

@stemis
  • Laravel Version: 8.x
  • PHP Version: 8.0RC5
  • Database Driver & Version: MariaDB

Description:

As this is a difficult subject I will try my best to explain it properly, it is an issue that needs to be addressed properly.

PHP 8.0 changes

In PHP 8.0, the pdo transaction logic was rewritten to some extend.
When issuing a PDO::rollback(), this will now throw a PDOException if the transaction was already closed.

* I was not able to find any official documentation on this yet, but was able to confirm this with the different PHP versions

Background Implicit Commits with Transactions

MySQL has a thing called implicit commits, to demonstrate what this does I have created the following example. I have used MariaDB as this has access to the in_transaction session var to demonstrate the working.

create table Table1
(
    name varchar(10) null
);

START TRANSACTION;

SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 1

INSERT INTO Table1 (name) values ('before');

SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 1

create table Table2
(
    name varchar(10) null
);

SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 0

INSERT INTO Table1 (name) values ('after');

SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 0
ROLLBACK ;

# Table 1 now contains both rows.

So imagine a UnitTest executing any Implicit commit commands, then it commits the transaction. So when running Migration command in a test, or stored procedures, or seeders that have implicit commands in them, a rollback is no longer possible as the transaction was already closed.
In php <=7.4.12 no exception was thrown, so most of the times, you would have no idea why you have data leakage in your test
PHP 8.0 now throwns an exception.

Laravel

How does this affect Laravel?

  1. Many applications use DatabaseTransactions or RefreshDatabase traits, which no longer work when implicit commits have been issued in a test.
  2. When a rollback is happening with DB::transaction, a new PDOException is thrown.

Consider the following test file:

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

class MyTest extends TestCase
{
    use DatabaseTransactions;
    
    public function testMe()
    {
        DB::unprepared('CREATE TABLE a (col varchar(1) null)');
    }
}

This will generate the following error when running the test:

There was 1 error:

1) Tests\Unit\MyTest::testMe
PDOException: There is no active transaction

/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:258
/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:237
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/DatabaseTransactions.php:31
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153

Dump of the exception object:

PDOException {#4783
  #message: "There is no active transaction"
  #code: 0
  #file: "./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php"
  #line: 258
  +errorInfo: null
  trace: {
    ./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:258 { …}
    ./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:237 { …}
    ./vendor/laravel/framework/src/Illuminate/Foundation/Testing/DatabaseTransactions.php:31 { …}
    ./vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237 { …}
    ./vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestCase.php:1188 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestResult.php:730 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestCase.php:883 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
    ./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
    ./vendor/phpunit/phpunit/src/TextUI/TestRunner.php:667 { …}
    ./vendor/phpunit/phpunit/src/TextUI/Command.php:148 { …}
    ./vendor/phpunit/phpunit/src/TextUI/Command.php:101 { …}
    ./vendor/phpunit/phpunit/phpunit:61 { …}
  }
}

Laravel code

Laravel calls the PHP's rollback command as follows:

try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}

protected function handleRollBackException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}

What has changed?

This exception was not thrown on PHP7.4.12, it is thrown on PHP 8.0RC5

I encountered this issue myself when trying to run my application on 8.0RC5, I can imagine a lot of others will encounter this exception.

Next steps?

Now the question arises what to do with the new Exception that is thrown? As it is currently quite vague as to why it is happening by just looking at the error message, and it cost me a lot of research to find out the cause of it.

I think we need to do at least the following:

  • Warn users that their test contains implicit commits leading to leaked data during test runs which greatly reduces their isolation.
  • Still have a way to "ignore" the exception to make sure there will be a smooth transition when moving to 8.0.
  • Think carefully about the effect of this new exception, how will this affect DB::transaction() when a rollback is initiated?

Please let me know if I can help you out in any way with this issue.

Steps to reproduce

Run the following on PHP 8:

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

class MyTest extends TestCase
{
    use DatabaseTransactions;
    
    public function testMe()
    {
        DB::unprepared('CREATE TABLE a (col varchar(1) null)');
    }
}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions