ext/sqlite3: explain statement support addition.

similar to what have been done for pdo/sqlite as having statement
explain support to simulate how a query would operate or
for more advanced users, analysing the VM routines used
for possible optimisations.

close GH-18853
This commit is contained in:
David Carlier 2025-06-15 00:04:30 +01:00
parent e9310171f7
commit 22bd2ae63f
No known key found for this signature in database
GPG key ID: 8486F847B4B94EF1
5 changed files with 500 additions and 1 deletions

2
NEWS
View file

@ -247,6 +247,8 @@ PHP NEWS
- Sqlite: - Sqlite:
. Added Sqlite3Stmt::busy to check if a statement is still being executed. . Added Sqlite3Stmt::busy to check if a statement is still being executed.
(David Carlier) (David Carlier)
. Added Sqlite3Stmt::explain to produce a explain query plan from
the statement. (David Carlier)
- Standard: - Standard:
. Fixed crypt() tests on musl when using --with-external-libcrypt . Fixed crypt() tests on musl when using --with-external-libcrypt

View file

@ -26,6 +26,9 @@
#include "SAPI.h" #include "SAPI.h"
#include <sqlite3.h> #include <sqlite3.h>
#ifdef __APPLE__
#include <Availability.h>
#endif
#include "zend_exceptions.h" #include "zend_exceptions.h"
#include "sqlite3_arginfo.h" #include "sqlite3_arginfo.h"
@ -1500,6 +1503,60 @@ PHP_METHOD(SQLite3Stmt, busy)
RETURN_FALSE; RETURN_FALSE;
} }
#if SQLITE_VERSION_NUMBER >= 3043000
PHP_METHOD(SQLite3Stmt, explain)
{
#ifdef __APPLE__
if (__builtin_available(macOS 14.2, *)) {
#endif
php_sqlite3_stmt *stmt_obj;
zval *object = ZEND_THIS;
stmt_obj = Z_SQLITE3_STMT_P(object);
ZEND_PARSE_PARAMETERS_NONE();
SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3);
SQLITE3_CHECK_INITIALIZED_STMT(stmt_obj->stmt, SQLite3Stmt);
RETURN_LONG((zend_long)sqlite3_stmt_isexplain(stmt_obj->stmt));
#ifdef __APPLE__
} else {
zend_throw_error(NULL, "explain statement unsupported");
}
#endif
}
PHP_METHOD(SQLite3Stmt, setExplain)
{
#ifdef __APPLE__
if (__builtin_available(macOS 14.2, *)) {
#endif
php_sqlite3_stmt *stmt_obj;
zend_long mode;
zval *object = ZEND_THIS;
stmt_obj = Z_SQLITE3_STMT_P(object);
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(mode)
ZEND_PARSE_PARAMETERS_END();
if (mode < 0 || mode > 2) {
zend_argument_value_error(1, "must be one of the SQLite3Stmt::EXPLAIN_MODE_* constants");
RETURN_THROWS();
}
SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3);
SQLITE3_CHECK_INITIALIZED_STMT(stmt_obj->stmt, SQLite3Stmt);
RETURN_BOOL(sqlite3_stmt_explain(stmt_obj->stmt, (int)mode) == SQLITE_OK);
#ifdef __APPLE__
} else {
zend_throw_error(NULL, "explain statement unsupported");
}
#endif
}
#endif
/* bind parameters to a statement before execution */ /* bind parameters to a statement before execution */
static int php_sqlite3_bind_params(php_sqlite3_stmt *stmt_obj) /* {{{ */ static int php_sqlite3_bind_params(php_sqlite3_stmt *stmt_obj) /* {{{ */
{ {

View file

@ -274,6 +274,15 @@ class SQLite3Stmt
public function reset(): bool {} public function reset(): bool {}
public function busy(): bool {} public function busy(): bool {}
#if SQLITE_VERSION_NUMBER >= 3043000
public const int EXPLAIN_MODE_PREPARED = 0;
public const int EXPLAIN_MODE_EXPLAIN = 1;
public const int EXPLAIN_MODE_EXPLAIN_QUERY_PLAN = 2;
public function explain(): int {}
public function setExplain(int $mode): bool {}
#endif
} }
/** @not-serializable */ /** @not-serializable */

View file

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead. /* This is a generated file, edit the .stub.php file instead.
* Stub hash: 28132e0e4df61f19dc4b23a7c9f79be6b3e40a8e */ * Stub hash: c3216eada9881743cbd3aa1510f1200b7ce0d942 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SQLite3___construct, 0, 0, 1) ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SQLite3___construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0)
@ -147,6 +147,15 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SQLite3Stmt_busy, 0, 0, _IS_BOOL, 0) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SQLite3Stmt_busy, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
#if SQLITE_VERSION_NUMBER >= 3043000
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SQLite3Stmt_explain, 0, 0, IS_LONG, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SQLite3Stmt_setExplain, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, mode, IS_LONG, 0)
ZEND_END_ARG_INFO()
#endif
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SQLite3Result___construct, 0, 0, 0) ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SQLite3Result___construct, 0, 0, 0)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
@ -206,6 +215,10 @@ ZEND_METHOD(SQLite3Stmt, paramCount);
ZEND_METHOD(SQLite3Stmt, readOnly); ZEND_METHOD(SQLite3Stmt, readOnly);
ZEND_METHOD(SQLite3Stmt, reset); ZEND_METHOD(SQLite3Stmt, reset);
ZEND_METHOD(SQLite3Stmt, busy); ZEND_METHOD(SQLite3Stmt, busy);
#if SQLITE_VERSION_NUMBER >= 3043000
ZEND_METHOD(SQLite3Stmt, explain);
ZEND_METHOD(SQLite3Stmt, setExplain);
#endif
ZEND_METHOD(SQLite3Result, __construct); ZEND_METHOD(SQLite3Result, __construct);
ZEND_METHOD(SQLite3Result, numColumns); ZEND_METHOD(SQLite3Result, numColumns);
ZEND_METHOD(SQLite3Result, columnName); ZEND_METHOD(SQLite3Result, columnName);
@ -258,6 +271,10 @@ static const zend_function_entry class_SQLite3Stmt_methods[] = {
ZEND_ME(SQLite3Stmt, readOnly, arginfo_class_SQLite3Stmt_readOnly, ZEND_ACC_PUBLIC) ZEND_ME(SQLite3Stmt, readOnly, arginfo_class_SQLite3Stmt_readOnly, ZEND_ACC_PUBLIC)
ZEND_ME(SQLite3Stmt, reset, arginfo_class_SQLite3Stmt_reset, ZEND_ACC_PUBLIC) ZEND_ME(SQLite3Stmt, reset, arginfo_class_SQLite3Stmt_reset, ZEND_ACC_PUBLIC)
ZEND_ME(SQLite3Stmt, busy, arginfo_class_SQLite3Stmt_busy, ZEND_ACC_PUBLIC) ZEND_ME(SQLite3Stmt, busy, arginfo_class_SQLite3Stmt_busy, ZEND_ACC_PUBLIC)
#if SQLITE_VERSION_NUMBER >= 3043000
ZEND_ME(SQLite3Stmt, explain, arginfo_class_SQLite3Stmt_explain, ZEND_ACC_PUBLIC)
ZEND_ME(SQLite3Stmt, setExplain, arginfo_class_SQLite3Stmt_setExplain, ZEND_ACC_PUBLIC)
#endif
ZEND_FE_END ZEND_FE_END
}; };
@ -540,6 +557,30 @@ static zend_class_entry *register_class_SQLite3Stmt(void)
INIT_CLASS_ENTRY(ce, "SQLite3Stmt", class_SQLite3Stmt_methods); INIT_CLASS_ENTRY(ce, "SQLite3Stmt", class_SQLite3Stmt_methods);
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE);
#if SQLITE_VERSION_NUMBER >= 3043000
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 >= 3043000
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 >= 3043000
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
return class_entry; return class_entry;
} }

View file

@ -0,0 +1,390 @@
--TEST--
Sqlite3Stmt::explain/setExplain usage
--EXTENSIONS--
sqlite3
--SKIPIF--
<?php
if (PHP_OS_FAMILY === "Darwin") die("skip on darwin for now");
$version = SQLite3::version()['versionNumber'];
if ($version <= 3043000) die("skip for sqlite3 < 3.43.0");
?>
--FILE--
<?php
require_once(__DIR__ . '/new_db.inc');
$db->exec('CREATE TABLE test_explain (a string);');
$stmt = $db->prepare('INSERT INTO test_explain VALUES ("first insert"), ("second_insert")');
$stmt->setExplain(Sqlite3Stmt::EXPLAIN_MODE_EXPLAIN);
var_dump($stmt->explain() == Sqlite3Stmt::EXPLAIN_MODE_EXPLAIN);
$r = $stmt->execute();
$result = [];
while (($arr = $r->fetchArray(SQLITE3_ASSOC)) !== false) $result[] = $arr;
var_dump($result);
$stmts = $db->prepare('SELECT * FROM test_explain');
$stmts->setExplain(Sqlite3Stmt::EXPLAIN_MODE_EXPLAIN_QUERY_PLAN);
$r = $stmts->execute();
$result = [];
while (($arr = $r->fetchArray(SQLITE3_ASSOC)) !== false) $result[] = $arr;
var_dump($result);
$stmt = $db->prepare('INSERT INTO test_explain VALUES ("first insert"), ("second_insert")');
$stmt->setExplain(Sqlite3Stmt::EXPLAIN_MODE_PREPARED);
$stmt->execute();
$stmts = $db->prepare('SELECT * FROM test_explain');
$stmts->setExplain(Sqlite3Stmt::EXPLAIN_MODE_PREPARED);
$r = $stmts->execute();
$result = [];
while (($arr = $r->fetchArray(SQLITE3_ASSOC)) !== false) $result[] = $arr;
var_dump($result);
try {
$stmts->setExplain(-1);
} catch (\ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
$stmts->setExplain(256);
} catch (\ValueError $e) {
echo $e->getMessage(), PHP_EOL;
}
var_dump($stmts->explain() == Sqlite3Stmt::EXPLAIN_MODE_PREPARED);
?>
--EXPECTF--
bool(true)
array(%d) {
[0]=>
array(8) {
["addr"]=>
int(0)
["opcode"]=>
string(4) "Init"
["p1"]=>
int(0)
["p2"]=>
int(%d)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[1]=>
array(8) {
["addr"]=>
int(1)
["opcode"]=>
string(13) "InitCoroutine"
["p1"]=>
int(3)
["p2"]=>
int(%d)
["p3"]=>
int(2)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
%A
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(7) "String8"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(12) "first insert"
["p5"]=>
int(0)
["comment"]=>
%a
}
%A
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
%A
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(7) "String8"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(13) "second_insert"
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(12) "EndCoroutine"
["p1"]=>
int(3)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(9) "OpenWrite"
["p1"]=>
int(0)
["p2"]=>
int(2)
["p3"]=>
int(0)
["p4"]=>
string(1) "1"
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(5) "Yield"
["p1"]=>
int(3)
["p2"]=>
int(%d)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(8) "NewRowid"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(10) "MakeRecord"
["p1"]=>
int(2)
["p2"]=>
int(1)
["p3"]=>
int(4)
["p4"]=>
string(1) "C"
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(6) "Insert"
["p1"]=>
int(0)
["p2"]=>
int(4)
["p3"]=>
int(1)
["p4"]=>
string(12) "test_explain"
["p5"]=>
int(57)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(4) "Goto"
["p1"]=>
int(0)
["p2"]=>
int(%d)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(4) "Halt"
["p1"]=>
int(0)
["p2"]=>
int(0)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(11) "Transaction"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(1)
["p4"]=>
string(1) "0"
["p5"]=>
int(1)
["comment"]=>
%a
}
[%d]=>
array(8) {
["addr"]=>
int(%d)
["opcode"]=>
string(4) "Goto"
["p1"]=>
int(0)
["p2"]=>
int(1)
["p3"]=>
int(0)
["p4"]=>
NULL
["p5"]=>
int(0)
["comment"]=>
%a
}
}
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"
}
}
SQLite3Stmt::setExplain(): Argument #1 ($mode) must be one of the SQLite3Stmt::EXPLAIN_MODE_* constants
SQLite3Stmt::setExplain(): Argument #1 ($mode) must be one of the SQLite3Stmt::EXPLAIN_MODE_* constants
bool(true)