1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

ext/pdo_sqlite: EXPLAIN mode support for SQL statements.

available since 3.41.0 we can reprepare a statement in either
 explain, explain query plan or the usual prepared mode.

close GH-18829
This commit is contained in:
David Carlier
2025-06-10 19:24:35 +01:00
parent 5740038026
commit 71a254489c
7 changed files with 511 additions and 3 deletions

3
NEWS
View File

@@ -148,6 +148,9 @@ PHP NEWS
has a wrong return type. (David Carlier)
. Added Pdo_Sqlite::ATTR_BUSY_STATEMENT constant to check
if a statement is currently executing. (David Carlier)
. Added Pdo_Sqlite::ATTR_EXPLAIN_STATEMENT constant to set a statement
in either EXPLAIN_MODE_PREPARED, EXPLAIN_MODE_EXPLAIN,
EXPLAIN_MODE_EXPLAIN_QUERY_PLAN modes. (David Carlier)
- PGSQL:
. Added pg_close_stmt to close a prepared statement while allowing

View File

@@ -198,6 +198,9 @@ PHP 8.5 UPGRADE NOTES
- PDO_Sqlite:
. Added class constant Pdo_Sqlite::ATTR_BUSY_STATEMENT.
. Added class constants Pdo_Sqlite::ATTR_EXPLAIN_STATEMENT,
Pdo_Sqlite::EXPLAIN_MODE_PREPARED, Pdo_Sqlite::EXPLAIN_MODE_EXPLAIN,
Pdo_Sqlite::EXPLAIN_MODE_EXPLAIN_QUERY_PLAN.
- SOAP:
. Enumeration cases are now dumped in __getTypes().

View File

@@ -36,6 +36,15 @@ class Sqlite extends \PDO
/** @cvalue PDO_SQLITE_ATTR_BUSY_STATEMENT */
public const int ATTR_BUSY_STATEMENT = UNKNOWN;
/** @cvalue PDO_SQLITE_ATTR_EXPLAIN_STATEMENT */
public const int ATTR_EXPLAIN_STATEMENT = UNKNOWN;
#if SQLITE_VERSION_NUMBER >= 3041000
public const int EXPLAIN_MODE_PREPARED = 0;
public const int EXPLAIN_MODE_EXPLAIN = 1;
public const int EXPLAIN_MODE_EXPLAIN_QUERY_PLAN = 2;
#endif
/** @cvalue SQLITE_OK */
public const int OK = UNKNOWN;

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: ae1e62d72c3c8290c9f39f21b583e980ea9b8eb2 */
* Stub hash: fa489a46c586ae935036f76a992163aeb67246d3 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Pdo_Sqlite_createAggregate, 0, 3, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
@@ -116,6 +116,36 @@ static zend_class_entry *register_class_Pdo_Sqlite(zend_class_entry *class_entry
zend_declare_typed_class_constant(class_entry, const_ATTR_BUSY_STATEMENT_name, &const_ATTR_BUSY_STATEMENT_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_ATTR_BUSY_STATEMENT_name);
zval const_ATTR_EXPLAIN_STATEMENT_value;
ZVAL_LONG(&const_ATTR_EXPLAIN_STATEMENT_value, PDO_SQLITE_ATTR_EXPLAIN_STATEMENT);
zend_string *const_ATTR_EXPLAIN_STATEMENT_name = zend_string_init_interned("ATTR_EXPLAIN_STATEMENT", sizeof("ATTR_EXPLAIN_STATEMENT") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_ATTR_EXPLAIN_STATEMENT_name, &const_ATTR_EXPLAIN_STATEMENT_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_ATTR_EXPLAIN_STATEMENT_name);
#if SQLITE_VERSION_NUMBER >= 3041000
zval const_EXPLAIN_MODE_PREPARED_value;
ZVAL_LONG(&const_EXPLAIN_MODE_PREPARED_value, 0);
zend_string *const_EXPLAIN_MODE_PREPARED_name = zend_string_init_interned("EXPLAIN_MODE_PREPARED", sizeof("EXPLAIN_MODE_PREPARED") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_EXPLAIN_MODE_PREPARED_name, &const_EXPLAIN_MODE_PREPARED_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_EXPLAIN_MODE_PREPARED_name);
#endif
#if SQLITE_VERSION_NUMBER >= 3041000
zval const_EXPLAIN_MODE_EXPLAIN_value;
ZVAL_LONG(&const_EXPLAIN_MODE_EXPLAIN_value, 1);
zend_string *const_EXPLAIN_MODE_EXPLAIN_name = zend_string_init_interned("EXPLAIN_MODE_EXPLAIN", sizeof("EXPLAIN_MODE_EXPLAIN") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_EXPLAIN_MODE_EXPLAIN_name, &const_EXPLAIN_MODE_EXPLAIN_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_EXPLAIN_MODE_EXPLAIN_name);
#endif
#if SQLITE_VERSION_NUMBER >= 3041000
zval const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_value;
ZVAL_LONG(&const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_value, 2);
zend_string *const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_name = zend_string_init_interned("EXPLAIN_MODE_EXPLAIN_QUERY_PLAN", sizeof("EXPLAIN_MODE_EXPLAIN_QUERY_PLAN") - 1, 1);
zend_declare_typed_class_constant(class_entry, const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_name, &const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release(const_EXPLAIN_MODE_EXPLAIN_QUERY_PLAN_name);
#endif
zval const_OK_value;
ZVAL_LONG(&const_OK_value, SQLITE_OK);
zend_string *const_OK_name = zend_string_init_interned("OK", sizeof("OK") - 1, 1);

View File

@@ -74,7 +74,8 @@ enum {
PDO_SQLITE_ATTR_OPEN_FLAGS = PDO_ATTR_DRIVER_SPECIFIC,
PDO_SQLITE_ATTR_READONLY_STATEMENT,
PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES,
PDO_SQLITE_ATTR_BUSY_STATEMENT
PDO_SQLITE_ATTR_BUSY_STATEMENT,
PDO_SQLITE_ATTR_EXPLAIN_STATEMENT
};
typedef int pdo_sqlite_create_collation_callback(void*, int, const void*, int, const void*);

View File

@@ -26,6 +26,11 @@
#include "php_pdo_sqlite.h"
#include "php_pdo_sqlite_int.h"
#if defined(__APPLE__)
// If more than one usage, a Zend macro could be created
// around this runtime check
#include <Availability.h>
#endif
static int pdo_sqlite_stmt_dtor(pdo_stmt_t *stmt)
{
@@ -387,6 +392,23 @@ static int pdo_sqlite_stmt_get_attribute(pdo_stmt_t *stmt, zend_long attr, zval
ZVAL_TRUE(val);
}
break;
case PDO_SQLITE_ATTR_EXPLAIN_STATEMENT:
#if SQLITE_VERSION_NUMBER >= 3041000
#if defined(__APPLE__)
if (__builtin_available(macOS 14.2, *)) {
#endif
ZVAL_LONG(val, (zend_long)sqlite3_stmt_isexplain(S->stmt));
return 1;
#if defined(__APPLE__)
} else {
zend_value_error("explain statement unsupported");
return 0;
}
#endif
#else
zend_value_error("explain statement unsupported");
return 0;
#endif
default:
return 0;
}
@@ -394,6 +416,46 @@ static int pdo_sqlite_stmt_get_attribute(pdo_stmt_t *stmt, zend_long attr, zval
return 1;
}
static int pdo_sqlite_stmt_set_attribute(pdo_stmt_t *stmt, zend_long attr, zval *zval)
{
pdo_sqlite_stmt *S = (pdo_sqlite_stmt*)stmt->driver_data;
switch (attr) {
case PDO_SQLITE_ATTR_EXPLAIN_STATEMENT:
#if SQLITE_VERSION_NUMBER >= 3041000
#if defined(__APPLE__)
if (__builtin_available(macOS 14.2, *)) {
#endif
if (Z_TYPE_P(zval) != IS_LONG) {
zend_type_error("explain mode must be of type int, %s given", zend_zval_value_name(zval));
return 0;
}
if (Z_LVAL_P(zval) < 0 || Z_LVAL_P(zval) > 2) {
zend_value_error("explain mode must be one of the EXPLAIN_MODE_* constants");
return 0;
}
if (sqlite3_stmt_explain(S->stmt, (int)Z_LVAL_P(zval)) != SQLITE_OK) {
return 0;
}
return 1;
#if defined(__APPLE__)
} else {
zend_value_error("explain statement unsupported");
return 0;
}
#endif
#else
zend_value_error("explain statement unsupported");
return 0;
#endif
default:
return 0;
}
return 1;
}
const struct pdo_stmt_methods sqlite_stmt_methods = {
pdo_sqlite_stmt_dtor,
pdo_sqlite_stmt_execute,
@@ -401,7 +463,7 @@ const struct pdo_stmt_methods sqlite_stmt_methods = {
pdo_sqlite_stmt_describe,
pdo_sqlite_stmt_get_col,
pdo_sqlite_stmt_param_hook,
NULL, /* set_attr */
pdo_sqlite_stmt_set_attribute, /* set_attr */
pdo_sqlite_stmt_get_attribute, /* get_attr */
pdo_sqlite_stmt_col_meta,
NULL, /* next_rowset */

View File

@@ -0,0 +1,400 @@
--TEST--
Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT usage
--EXTENSIONS--
pdo_sqlite
--SKIPIF--
<?php
if (PHP_OS_FAMILY === "Darwin") die("skip on darwin for now");
if (!defined('Pdo\Sqlite::EXPLAIN_MODE_EXPLAIN')) die('skip system sqlite does not support EXPLAIN mode');
?>
--FILE--
<?php
$db = new Pdo\Sqlite('sqlite::memory:');
$db->query('CREATE TABLE test_explain (a string);');
$stmt = $db->prepare('INSERT INTO test_explain VALUES ("first insert"), ("second_insert")');
$stmt->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, Pdo\Sqlite::EXPLAIN_MODE_EXPLAIN);
var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT) == Pdo\Sqlite::EXPLAIN_MODE_EXPLAIN);
$r = $stmt->execute();
var_dump($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmts = $db->prepare('SELECT * FROM test_explain');
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, Pdo\Sqlite::EXPLAIN_MODE_EXPLAIN_QUERY_PLAN);
var_dump($stmt->getAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT) == Pdo\Sqlite::EXPLAIN_MODE_EXPLAIN_QUERY_PLAN);
$r = $stmts->execute();
var_dump($stmts->fetchAll(PDO::FETCH_ASSOC));
$stmt = $db->prepare('INSERT INTO test_explain VALUES ("first insert"), ("second_insert")');
$stmt->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, Pdo\Sqlite::EXPLAIN_MODE_PREPARED);
$stmt->execute();
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, Pdo\Sqlite::EXPLAIN_MODE_PREPARED);
$r = $stmts->execute();
var_dump($stmts->fetchAll(PDO::FETCH_ASSOC));
class Duh {}
try {
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, "EXPLAIN");
} catch (\TypeError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, new Duh());
} catch (\TypeError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, -1);
} catch (\ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
$stmts->setAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT, 256);
} catch (\ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
var_dump($stmts->getAttribute(Pdo\Sqlite::ATTR_EXPLAIN_STATEMENT) == Pdo\Sqlite::EXPLAIN_MODE_PREPARED);
?>
--EXPECT--
bool(true)
array(16) {
[0]=>
array(8) {
["addr"]=>
int(0)
["opcode"]=>
string(4) "Init"
["p1"]=>
int(0)
["p2"]=>
int(14)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[1]=>
array(8) {
["addr"]=>
int(1)
["opcode"]=>
string(13) "InitCoroutine"
["p1"]=>
int(3)
["p2"]=>
int(7)
["p3"]=>
int(2)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[2]=>
array(8) {
["addr"]=>
int(2)
["opcode"]=>
string(7) "String8"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(12) "first insert"
["p5"]=>
int(0)
["comment"]=>
NULL
}
[3]=>
array(8) {
["addr"]=>
int(3)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[4]=>
array(8) {
["addr"]=>
int(4)
["opcode"]=>
string(7) "String8"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(13) "second_insert"
["p5"]=>
int(0)
["comment"]=>
NULL
}
[5]=>
array(8) {
["addr"]=>
int(5)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[6]=>
array(8) {
["addr"]=>
int(6)
["opcode"]=>
string(12) "EndCoroutine"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[7]=>
array(8) {
["addr"]=>
int(7)
["opcode"]=>
string(9) "OpenWrite"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(1) "1"
["p5"]=>
int(0)
["comment"]=>
NULL
}
[8]=>
array(8) {
["addr"]=>
int(8)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(13)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[9]=>
array(8) {
["addr"]=>
int(9)
["opcode"]=>
string(8) "NewRowid"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[10]=>
array(8) {
["addr"]=>
int(10)
["opcode"]=>
string(10) "MakeRecord"
["p1"]=>
int(2)
["p2"]=>
int(1)
["p3"]=>
int(4)
["p4"]=>
string(1) "C"
["p5"]=>
int(0)
["comment"]=>
NULL
}
[11]=>
array(8) {
["addr"]=>
int(11)
["opcode"]=>
string(6) "Insert"
["p1"]=>
int(0)
["p2"]=>
int(4)
["p3"]=>
int(1)
["p4"]=>
string(12) "test_explain"
["p5"]=>
int(57)
["comment"]=>
NULL
}
[12]=>
array(8) {
["addr"]=>
int(12)
["opcode"]=>
string(4) "Goto"
["p1"]=>
int(0)
["p2"]=>
int(8)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[13]=>
array(8) {
["addr"]=>
int(13)
["opcode"]=>
string(4) "Halt"
["p1"]=>
int(0)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
[14]=>
array(8) {
["addr"]=>
int(14)
["opcode"]=>
string(11) "Transaction"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(1)
["p4"]=>
string(1) "0"
["p5"]=>
int(1)
["comment"]=>
NULL
}
[15]=>
array(8) {
["addr"]=>
int(15)
["opcode"]=>
string(4) "Goto"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
NULL
}
}
bool(false)
array(1) {
[0]=>
array(4) {
["id"]=>
int(2)
["parent"]=>
int(0)
["notused"]=>
int(0)
["detail"]=>
string(17) "SCAN test_explain"
}
}
array(2) {
[0]=>
array(1) {
["a"]=>
string(12) "first insert"
}
[1]=>
array(1) {
["a"]=>
string(13) "second_insert"
}
}
explain mode must be of type int, string given
explain mode must be of type int, Duh given
explain mode must be one of the EXPLAIN_MODE_* constants
explain mode must be one of the EXPLAIN_MODE_* constants
bool(true)