Skip to content

Implement GH-8967: Add PDO_SQLITE_ATTR_TRANSACTION_MODE #19317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ext/pdo_sqlite/pdo_sqlite.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ class Sqlite extends \PDO
/** @cvalue PDO_SQLITE_ATTR_EXPLAIN_STATEMENT */
public const int ATTR_EXPLAIN_STATEMENT = UNKNOWN;

/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE */
public const int ATTR_TRANSACTION_MODE = UNKNOWN;

/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED */
public const int ATTR_TRANSACTION_MODE_DEFERRED = UNKNOWN;

/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE */
public const int ATTR_TRANSACTION_MODE_IMMEDIATE = UNKNOWN;

/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE */
public const int ATTR_TRANSACTION_MODE_EXCLUSIVE = UNKNOWN;

#if SQLITE_VERSION_NUMBER >= 3043000
public const int EXPLAIN_MODE_PREPARED = 0;
public const int EXPLAIN_MODE_EXPLAIN = 1;
Expand Down
26 changes: 25 additions & 1 deletion ext/pdo_sqlite/pdo_sqlite_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion ext/pdo_sqlite/php_pdo_sqlite_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ typedef struct {
struct pdo_sqlite_func *funcs;
struct pdo_sqlite_collation *collations;
zend_fcall_info_cache authorizer_fcc;
zend_long transaction_mode;
} pdo_sqlite_db_handle;

typedef struct {
Expand All @@ -75,7 +76,11 @@ enum {
PDO_SQLITE_ATTR_READONLY_STATEMENT,
PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES,
PDO_SQLITE_ATTR_BUSY_STATEMENT,
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT,
PDO_SQLITE_ATTR_TRANSACTION_MODE,
PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED,
PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE,
PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE
};

typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*);
Expand Down
41 changes: 40 additions & 1 deletion ext/pdo_sqlite/sqlite_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,23 @@ static bool sqlite_handle_begin(pdo_dbh_t *dbh)
{
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;

if (sqlite3_exec(H->db, "BEGIN", NULL, NULL, NULL) != SQLITE_OK) {
char *begin_statement;
switch (H->transaction_mode) {
case 0:
case PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED:
begin_statement = "BEGIN DEFERRED TRANSACTION";
break;
Comment on lines +260 to +263
Copy link
Author

@stancl stancl Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0 case is when no transaction mode has been set, so we use the default (deferred).

Is it a safe assumption that driver_data is always zero-initialized? In the code I could find, I saw that driver_data is allocated with pecalloc().

case PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE:
begin_statement = "BEGIN IMMEDIATE TRANSACTION";
break;
case PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE:
begin_statement = "BEGIN EXCLUSIVE TRANSACTION";
break;
default:
begin_statement = "BEGIN";
}

if (sqlite3_exec(H->db, begin_statement, NULL, NULL, NULL) != SQLITE_OK) {
pdo_sqlite_error(dbh);
return false;
}
Expand Down Expand Up @@ -286,11 +302,22 @@ static bool sqlite_handle_rollback(pdo_dbh_t *dbh)

static int pdo_sqlite_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
{
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;
zend_long transaction_mode;

switch (attr) {
case PDO_ATTR_CLIENT_VERSION:
case PDO_ATTR_SERVER_VERSION:
ZVAL_STRING(return_value, (char *)sqlite3_libversion());
break;
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
if (H->transaction_mode == 0) {
transaction_mode = PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED;
} else {
transaction_mode = H->transaction_mode;
}
ZVAL_LONG(return_value, transaction_mode);
break;

default:
return 0;
Expand Down Expand Up @@ -326,6 +353,18 @@ static bool pdo_sqlite_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
}
sqlite3_extended_result_codes(H->db, lval);
return true;
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
if (!pdo_get_long_param(&lval, val)) {
return false;
}
if (lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED &&
lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_IMMEDIATE &&
lval != PDO_SQLITE_ATTR_TRANSACTION_MODE_EXCLUSIVE) {
return false;
}
H->transaction_mode = lval;
return true;

}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
--TEST--
PDO_sqlite: Testing ATTR_TRANSACTION_MODE
--EXTENSIONS--
pdo_sqlite
--FILE--
<?php

$dsn = 'sqlite:file:foo?mode=memory&cache=shared';
$pdo = PDO::connect($dsn);
$pdo2 = PDO::connect($dsn);

// Deferred by default before any transaction mode is set
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED);

// Both should return true
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED);

// Both should return true
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE);

// Both should return true
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE));
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE);

// Cannot set invalid values
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 0));
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 1));
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 123));

// Cannot use these as keys, only as values. These should return false
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED, true));
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE, true));
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE, true));

// Set $pdo to deferred, try to get immediate transaction in $pdo2. There should be no lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_DEFERRED);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
$pdo2->rollBack();
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
$pdo->rollBack();

// Set $pdo to immediate, try to get immediate transaction in $pdo2. There SHOULD be lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_IMMEDIATE);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
$pdo->rollBack();

// Set $pdo to exclusive, try to get immediate transaction in $pdo2. There SHOULD be lock contention
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::ATTR_TRANSACTION_MODE_EXCLUSIVE);
$pdo->beginTransaction();
try {
$pdo2->exec('begin immediate transaction');
printf("Database is not locked\n");
} catch (PDOException $e) {
printf("Database is locked: %s\n", $e->getMessage());
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
Database is not locked
Database is locked: SQLSTATE[HY000]: General error: 6 database table is locked
Database is locked: SQLSTATE[HY000]: General error: 6 database schema is locked: main