Skip to content

Commit 1dc51c7

Browse files
Implement mysqli_execute_query() (#8660)
1 parent b45cd10 commit 1dc51c7

File tree

5 files changed

+261
-4
lines changed

5 files changed

+261
-4
lines changed

ext/mysqli/mysqli.stub.php

+7
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ public function debug(string $options) {} // TODO make return type void
208208
*/
209209
public function get_charset(): ?object {}
210210

211+
/**
212+
* @alias mysqli_execute_query
213+
*/
214+
public function execute_query(string $query, ?array $params = null): mysqli_result|bool {}
215+
211216
/**
212217
* @tentative-return-type
213218
* @alias mysqli_get_client_info
@@ -793,6 +798,8 @@ function mysqli_stmt_execute(mysqli_stmt $statement, ?array $params = null): boo
793798
/** @alias mysqli_stmt_execute */
794799
function mysqli_execute(mysqli_stmt $statement, ?array $params = null): bool {}
795800

801+
function mysqli_execute_query(mysqli $mysql, string $query, ?array $params = null): mysqli_result|bool {}
802+
796803
/** @refcount 1 */
797804
function mysqli_fetch_field(mysqli_result $result): object|false {}
798805

ext/mysqli/mysqli_api.c

+141-3
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,146 @@ PHP_FUNCTION(mysqli_stmt_execute)
476476
}
477477
/* }}} */
478478

479+
void close_stmt_and_copy_errors(MY_STMT *stmt, MY_MYSQL *mysql)
480+
{
481+
/* mysql_stmt_close() clears errors, so we have to store them temporarily */
482+
MYSQLND_ERROR_INFO error_info = *stmt->stmt->data->error_info;
483+
stmt->stmt->data->error_info->error_list.head = NULL;
484+
stmt->stmt->data->error_info->error_list.tail = NULL;
485+
stmt->stmt->data->error_info->error_list.count = 0;
486+
487+
/* we also remember affected_rows which gets cleared too */
488+
uint64_t affected_rows = mysql->mysql->data->upsert_status->affected_rows;
489+
490+
mysqli_stmt_close(stmt->stmt, false);
491+
stmt->stmt = NULL;
492+
php_clear_stmt_bind(stmt);
493+
494+
/* restore error messages, but into the mysql object */
495+
zend_llist_clean(&mysql->mysql->data->error_info->error_list);
496+
*mysql->mysql->data->error_info = error_info;
497+
mysql->mysql->data->upsert_status->affected_rows = affected_rows;
498+
}
499+
500+
PHP_FUNCTION(mysqli_execute_query)
501+
{
502+
MY_MYSQL *mysql;
503+
MY_STMT *stmt;
504+
char *query = NULL;
505+
size_t query_len;
506+
zval *mysql_link;
507+
HashTable *input_params = NULL;
508+
MYSQL_RES *result;
509+
MYSQLI_RESOURCE *mysqli_resource;
510+
511+
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "Os|h!", &mysql_link, mysqli_link_class_entry, &query, &query_len, &input_params) == FAILURE) {
512+
RETURN_THROWS();
513+
}
514+
MYSQLI_FETCH_RESOURCE_CONN(mysql, mysql_link, MYSQLI_STATUS_VALID);
515+
516+
stmt = (MY_STMT *)ecalloc(1,sizeof(MY_STMT));
517+
518+
if (!(stmt->stmt = mysql_stmt_init(mysql->mysql))) {
519+
MYSQLI_REPORT_MYSQL_ERROR(mysql->mysql);
520+
efree(stmt);
521+
RETURN_FALSE;
522+
}
523+
524+
if (FAIL == mysql_stmt_prepare(stmt->stmt, query, query_len)) {
525+
MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
526+
527+
close_stmt_and_copy_errors(stmt, mysql);
528+
RETURN_FALSE;
529+
}
530+
531+
/* The bit below, which is copied from mysqli_prepare, is needed for bad index exceptions */
532+
/* don't initialize stmt->query with NULL, we ecalloc()-ed the memory */
533+
/* Get performance boost if reporting is switched off */
534+
if (query_len && (MyG(report_mode) & MYSQLI_REPORT_INDEX)) {
535+
stmt->query = estrdup(query);
536+
}
537+
538+
// bind-in-execute
539+
// It's very similar to the mysqli_stmt::execute, but it uses different error handling
540+
if (input_params) {
541+
zval *tmp;
542+
unsigned int index;
543+
unsigned int hash_num_elements;
544+
unsigned int param_count;
545+
MYSQLND_PARAM_BIND *params;
546+
547+
if (!zend_array_is_list(input_params)) {
548+
mysqli_stmt_close(stmt->stmt, false);
549+
stmt->stmt = NULL;
550+
efree(stmt);
551+
zend_argument_value_error(ERROR_ARG_POS(3), "must be a list array");
552+
RETURN_THROWS();
553+
}
554+
555+
hash_num_elements = zend_hash_num_elements(input_params);
556+
param_count = mysql_stmt_param_count(stmt->stmt);
557+
if (hash_num_elements != param_count) {
558+
mysqli_stmt_close(stmt->stmt, false);
559+
stmt->stmt = NULL;
560+
efree(stmt);
561+
zend_argument_value_error(ERROR_ARG_POS(3), "must consist of exactly %d elements, %d present", param_count, hash_num_elements);
562+
RETURN_THROWS();
563+
}
564+
565+
params = mysqlnd_stmt_alloc_param_bind(stmt->stmt);
566+
ZEND_ASSERT(params);
567+
568+
index = 0;
569+
ZEND_HASH_FOREACH_VAL(input_params, tmp) {
570+
ZVAL_COPY_VALUE(&params[index].zv, tmp);
571+
params[index].type = MYSQL_TYPE_VAR_STRING;
572+
index++;
573+
} ZEND_HASH_FOREACH_END();
574+
575+
if (mysqlnd_stmt_bind_param(stmt->stmt, params)) {
576+
close_stmt_and_copy_errors(stmt, mysql);
577+
RETURN_FALSE;
578+
}
579+
580+
}
581+
582+
if (mysql_stmt_execute(stmt->stmt)) {
583+
MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
584+
585+
if (MyG(report_mode) & MYSQLI_REPORT_INDEX) {
586+
php_mysqli_report_index(stmt->query, mysqli_stmt_server_status(stmt->stmt));
587+
}
588+
589+
close_stmt_and_copy_errors(stmt, mysql);
590+
RETURN_FALSE;
591+
}
592+
593+
if (!mysql_stmt_field_count(stmt->stmt)) {
594+
/* no result set - not a SELECT */
595+
close_stmt_and_copy_errors(stmt, mysql);
596+
RETURN_TRUE;
597+
}
598+
599+
if (MyG(report_mode) & MYSQLI_REPORT_INDEX) {
600+
php_mysqli_report_index(stmt->query, mysqli_stmt_server_status(stmt->stmt));
601+
}
602+
603+
/* get result */
604+
if (!(result = mysqlnd_stmt_get_result(stmt->stmt))) {
605+
MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
606+
607+
close_stmt_and_copy_errors(stmt, mysql);
608+
RETURN_FALSE;
609+
}
610+
611+
mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE));
612+
mysqli_resource->ptr = (void *)result;
613+
mysqli_resource->status = MYSQLI_STATUS_VALID;
614+
MYSQLI_RETVAL_RESOURCE(mysqli_resource, mysqli_result_class_entry);
615+
616+
close_stmt_and_copy_errors(stmt, mysql);
617+
}
618+
479619
/* {{{ mixed mysqli_stmt_fetch_mysqlnd */
480620
void mysqli_stmt_fetch_mysqlnd(INTERNAL_FUNCTION_PARAMETERS)
481621
{
@@ -1188,9 +1328,7 @@ PHP_FUNCTION(mysqli_prepare)
11881328
/* don't initialize stmt->query with NULL, we ecalloc()-ed the memory */
11891329
/* Get performance boost if reporting is switched off */
11901330
if (stmt->stmt && query_len && (MyG(report_mode) & MYSQLI_REPORT_INDEX)) {
1191-
stmt->query = (char *)emalloc(query_len + 1);
1192-
memcpy(stmt->query, query, query_len);
1193-
stmt->query[query_len] = '\0';
1331+
stmt->query = estrdup(query);
11941332
}
11951333

11961334
/* don't join to the previous if because it won't work if mysql_stmt_prepare_fails */

ext/mysqli/mysqli_arginfo.h

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/mysqli/tests/mysqli_class_mysqli_interface.phpt

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require_once('skipifconnectfailure.inc');
2929
'connect' => true,
3030
'dump_debug_info' => true,
3131
'escape_string' => true,
32+
'execute_query' => true,
3233
'get_charset' => true,
3334
'get_client_info' => true,
3435
'get_server_info' => true,
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
--TEST--
2+
mysqli_execute_query()
3+
--EXTENSIONS--
4+
mysqli
5+
--SKIPIF--
6+
<?php
7+
require_once 'skipifconnectfailure.inc';
8+
?>
9+
--FILE--
10+
<?php
11+
12+
require 'table.inc';
13+
14+
if (!($tmp = mysqli_execute_query($link, "SELECT id, label FROM test ORDER BY id"))) {
15+
printf("[001] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
16+
}
17+
18+
if (!is_a($tmp, mysqli_result::class)) {
19+
printf("[002] Expecting mysqli_result, got %s/%s\n", gettype($tmp), $tmp);
20+
}
21+
22+
unset($tmp);
23+
24+
// procedural
25+
if (!($tmp = mysqli_execute_query($link, "SELECT ? AS a, ? AS b, ? AS c", [42, "foo", null]))) {
26+
printf("[003] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
27+
}
28+
29+
assert($tmp->fetch_assoc() === ['a' => '42', 'b' => 'foo', 'c' => null]);
30+
31+
// OO style
32+
if (!($tmp = $link->execute_query("SELECT ? AS a, ? AS b, ? AS c", [42, "foo", null]))) {
33+
printf("[004] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
34+
}
35+
36+
assert($tmp->fetch_assoc() === ['a' => '42', 'b' => 'foo', 'c' => null]);
37+
38+
// prepare error
39+
if (!($tmp = $link->execute_query("some random gibberish", [1, "foo"]))) {
40+
printf("[005] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
41+
}
42+
43+
// stmt error - duplicate key
44+
if (!$link->execute_query("INSERT INTO test(id, label) VALUES (?, ?)", [1, "foo"])) {
45+
printf("[006] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
46+
}
47+
48+
// successful update returns true
49+
if (!($tmp = $link->execute_query("UPDATE test SET label=? WHERE id=?", ["baz", 1]))) {
50+
printf("[007] Expecting true, got %s/%s\n", gettype($tmp), $tmp);
51+
}
52+
if ($link->affected_rows <= 0) {
53+
printf("[008] Expecting positive non-zero integer for affected_rows, got %s/%s\n", gettype($link->affected_rows), $link->affected_rows);
54+
}
55+
56+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
57+
58+
// check if value error is properly reported
59+
try {
60+
$link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo', 42]);
61+
} catch (ValueError $e) {
62+
echo '[009] '.$e->getMessage()."\n";
63+
}
64+
try {
65+
$link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo' => 42]);
66+
} catch (ValueError $e) {
67+
echo '[010] '.$e->getMessage()."\n";
68+
}
69+
70+
// check if insert_id is copied
71+
$link->execute_query("ALTER TABLE test MODIFY id INT NOT NULL AUTO_INCREMENT");
72+
$link->execute_query("INSERT INTO test(label) VALUES (?)", ["foo"]);
73+
if ($link->insert_id <= 0) {
74+
printf("[011] Expecting positive non-zero integer for insert_id, got %s/%s\n", gettype($link->insert_id), $link->insert_id);
75+
}
76+
77+
// bad index
78+
mysqli_report(MYSQLI_REPORT_ALL);
79+
try {
80+
$link->execute_query("SELECT id FROM test WHERE label = ?", ["foo"]);
81+
} catch (mysqli_sql_exception $e) {
82+
echo '[012] '.$e->getMessage()."\n";
83+
}
84+
85+
print "done!";
86+
?>
87+
--CLEAN--
88+
<?php
89+
require_once "clean_table.inc";
90+
?>
91+
--EXPECTF--
92+
[005] [1064] You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'some random gibberish' at line 1
93+
[006] [1062] Duplicate entry '1' for key '%s'
94+
[009] mysqli::execute_query(): Argument #2 ($params) must consist of exactly 3 elements, 2 present
95+
[010] mysqli::execute_query(): Argument #2 ($params) must be a list array
96+
[012] No index used in query/prepared statement SELECT id FROM test WHERE label = ?
97+
done!

0 commit comments

Comments
 (0)