Skip to content

Commit 585ab46

Browse files
committed
Add Pdo\Sqlite::ATTR_TRANSACTION_MODE
This commit implements GH-8967. SQLite supports multiple transaction modes. These include: - DEFERRED (default) only acquires a lock when you start a read/write - IMMEDIATE acquires a reserved lock - EXCLUSIVE acquires an exclusive lock (stricter than immediate) In WAL mode IMMEDIATE and EXCLUSIVE are identical. One reason for wanting to specify a transaction mode is that SQLite doesn't respect busy_timeout when a DEFERRED transaction tries to upgrade a read lock to a write lock. Normally if you try to acquire a lock and have busy_timeout configured, SQLite will wait for that period until giving up and erroring out (SQLITE_BUSY). With DEFERRED, if you have a transaction that first reads and there's a concurrent writer while it's trying to upgrade to a write lock, you will immediately get SQLITE_BUSY regardless of your busy_timeout. Prior to this commit, the only available workarounds were: - Using $pdo->exec("BEGIN IMMEDIATE TRANSACTION") instead of $pdo->beginTransaction() - Doing a dummy write at the start of each transaction so you don't get stuck with a read lock Both of those aren't very usable, especially in a framework context where the user doesn't have complete control over how transactions are started. To address that, this commit adds four class constants to Pdo\Sqlite: - ATTR_TRANSACTION_MODE -- a new attribute - TRANSACTION_MODE_DEFERRED = 0 - TRANSACTION_MODE_IMMEDIATE = 1 - TRANSACTION_MODE_EXCLUSIVE = 2 These can be used as: $pdo->setAttribute( $pdo::ATTR_TRANSACTION_MODE, $pdo::TRANSACTION_MODE_IMMEDIATE );
1 parent 066553c commit 585ab46

File tree

6 files changed

+161
-3
lines changed

6 files changed

+161
-3
lines changed

ext/pdo_sqlite/pdo_sqlite.stub.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class Sqlite extends \PDO
3939
/** @cvalue PDO_SQLITE_ATTR_EXPLAIN_STATEMENT */
4040
public const int ATTR_EXPLAIN_STATEMENT = UNKNOWN;
4141

42+
/** @cvalue PDO_SQLITE_ATTR_TRANSACTION_MODE */
43+
public const int ATTR_TRANSACTION_MODE = UNKNOWN;
44+
45+
public const int TRANSACTION_MODE_DEFERRED = 0;
46+
public const int TRANSACTION_MODE_IMMEDIATE = 1;
47+
public const int TRANSACTION_MODE_EXCLUSIVE = 2;
48+
4249
#if SQLITE_VERSION_NUMBER >= 3043000
4350
public const int EXPLAIN_MODE_PREPARED = 0;
4451
public const int EXPLAIN_MODE_EXPLAIN = 1;

ext/pdo_sqlite/pdo_sqlite_arginfo.h

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pdo_sqlite/php_pdo_sqlite.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ extern zend_module_entry pdo_sqlite_module_entry;
2727
#include "TSRM.h"
2828
#endif
2929

30+
enum pdo_sqlite_transaction_mode {
31+
PDO_SQLITE_TRANSACTION_MODE_DEFERRED = 0,
32+
PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE = 1,
33+
PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE = 2
34+
};
35+
3036
PHP_MINIT_FUNCTION(pdo_sqlite);
3137
PHP_MSHUTDOWN_FUNCTION(pdo_sqlite);
3238
PHP_RINIT_FUNCTION(pdo_sqlite);

ext/pdo_sqlite/php_pdo_sqlite_int.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ typedef struct {
5151
struct pdo_sqlite_func *funcs;
5252
struct pdo_sqlite_collation *collations;
5353
zend_fcall_info_cache authorizer_fcc;
54+
enum pdo_sqlite_transaction_mode transaction_mode;
5455
} pdo_sqlite_db_handle;
5556

5657
typedef struct {
@@ -75,7 +76,8 @@ enum {
7576
PDO_SQLITE_ATTR_READONLY_STATEMENT,
7677
PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES,
7778
PDO_SQLITE_ATTR_BUSY_STATEMENT,
78-
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT
79+
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT,
80+
PDO_SQLITE_ATTR_TRANSACTION_MODE
7981
};
8082

8183
typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*);

ext/pdo_sqlite/sqlite_driver.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,20 @@ static bool sqlite_handle_begin(pdo_dbh_t *dbh)
255255
{
256256
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;
257257

258-
if (sqlite3_exec(H->db, "BEGIN", NULL, NULL, NULL) != SQLITE_OK) {
258+
const char *begin_statement = "BEGIN";
259+
switch (H->transaction_mode) {
260+
case PDO_SQLITE_TRANSACTION_MODE_DEFERRED:
261+
begin_statement = "BEGIN DEFERRED TRANSACTION";
262+
break;
263+
case PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE:
264+
begin_statement = "BEGIN IMMEDIATE TRANSACTION";
265+
break;
266+
case PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE:
267+
begin_statement = "BEGIN EXCLUSIVE TRANSACTION";
268+
break;
269+
}
270+
271+
if (sqlite3_exec(H->db, begin_statement, NULL, NULL, NULL) != SQLITE_OK) {
259272
pdo_sqlite_error(dbh);
260273
return false;
261274
}
@@ -286,11 +299,16 @@ static bool sqlite_handle_rollback(pdo_dbh_t *dbh)
286299

287300
static int pdo_sqlite_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
288301
{
302+
pdo_sqlite_db_handle *H = (pdo_sqlite_db_handle *)dbh->driver_data;
303+
289304
switch (attr) {
290305
case PDO_ATTR_CLIENT_VERSION:
291306
case PDO_ATTR_SERVER_VERSION:
292307
ZVAL_STRING(return_value, (char *)sqlite3_libversion());
293308
break;
309+
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
310+
ZVAL_LONG(return_value, H->transaction_mode);
311+
break;
294312

295313
default:
296314
return 0;
@@ -326,6 +344,19 @@ static bool pdo_sqlite_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
326344
}
327345
sqlite3_extended_result_codes(H->db, lval);
328346
return true;
347+
case PDO_SQLITE_ATTR_TRANSACTION_MODE:
348+
if (!pdo_get_long_param(&lval, val)) {
349+
return false;
350+
}
351+
switch (lval) {
352+
case PDO_SQLITE_TRANSACTION_MODE_DEFERRED:
353+
case PDO_SQLITE_TRANSACTION_MODE_IMMEDIATE:
354+
case PDO_SQLITE_TRANSACTION_MODE_EXCLUSIVE:
355+
H->transaction_mode = lval;
356+
return true;
357+
default:
358+
return false;
359+
}
329360
}
330361
return false;
331362
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
--TEST--
2+
PDO_sqlite: Testing ATTR_TRANSACTION_MODE
3+
--EXTENSIONS--
4+
pdo_sqlite
5+
--FILE--
6+
<?php
7+
8+
$dsn = 'sqlite:file:foo?mode=memory&cache=shared';
9+
$pdo = PDO::connect($dsn);
10+
$pdo2 = PDO::connect($dsn);
11+
12+
// Deferred by default before any transaction mode is set
13+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
14+
15+
// Both should return true - setting DEFERRED
16+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_DEFERRED));
17+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
18+
19+
// Both should return true - setting IMMEDIATE
20+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE));
21+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
22+
23+
// Both should return true - setting EXCLUSIVE
24+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE));
25+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
26+
27+
// Setting the numeric equivalents of the above. All should return true
28+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 0));
29+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
30+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 1));
31+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
32+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 2));
33+
var_dump($pdo->getAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE) === PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
34+
35+
// Cannot set a random numeric value
36+
var_dump($pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, 4));
37+
38+
// Set $pdo to deferred, try to get immediate transaction in $pdo2. There should be no lock contention
39+
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_DEFERRED);
40+
$pdo->beginTransaction();
41+
try {
42+
$pdo2->exec('begin immediate transaction');
43+
$pdo2->rollBack();
44+
printf("Database is not locked\n");
45+
} catch (PDOException $e) {
46+
printf("Database is locked: %s\n", $e->getMessage());
47+
}
48+
$pdo->rollBack();
49+
50+
// Set $pdo to immediate, try to get immediate transaction in $pdo2. There SHOULD be lock contention
51+
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_IMMEDIATE);
52+
$pdo->beginTransaction();
53+
try {
54+
$pdo2->exec('begin immediate transaction');
55+
printf("Database is not locked\n");
56+
} catch (PDOException $e) {
57+
printf("Database is locked: %s\n", $e->getMessage());
58+
}
59+
$pdo->rollBack();
60+
61+
// Set $pdo to exclusive, try to get immediate transaction in $pdo2. There SHOULD be lock contention
62+
$pdo->setAttribute(PDO\Sqlite::ATTR_TRANSACTION_MODE, PDO\Sqlite::TRANSACTION_MODE_EXCLUSIVE);
63+
$pdo->beginTransaction();
64+
try {
65+
$pdo2->exec('begin immediate transaction');
66+
printf("Database is not locked\n");
67+
} catch (PDOException $e) {
68+
printf("Database is locked: %s\n", $e->getMessage());
69+
}
70+
?>
71+
--EXPECT--
72+
bool(true)
73+
bool(true)
74+
bool(true)
75+
bool(true)
76+
bool(true)
77+
bool(true)
78+
bool(true)
79+
bool(true)
80+
bool(true)
81+
bool(true)
82+
bool(true)
83+
bool(true)
84+
bool(true)
85+
bool(false)
86+
Database is not locked
87+
Database is locked: SQLSTATE[HY000]: General error: 6 database table is locked
88+
Database is locked: SQLSTATE[HY000]: General error: 6 database schema is locked: main

0 commit comments

Comments
 (0)