Skip to content

Commit 9126634

Browse files
committed
sqlite3: Fix double execution of queries when fetchArray is called
Introduces a flag which indicates to the fetch function that there is data from a previous call to sqlite3_step that has not yet been surfaced to the running script. When the flag is set, a round of calling sqlite3_step will be skipped. Closes 64531. <https://bugs.php.net/bug.php?id=64531>
1 parent d9c49ae commit 9126634

File tree

3 files changed

+248
-8
lines changed

3 files changed

+248
-8
lines changed

ext/sqlite3/php_sqlite3_structs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ struct _php_sqlite3_stmt_object {
133133
/* Keep track of the zvals for bound parameters */
134134
HashTable *bound_params;
135135
zend_object zo;
136+
137+
/* Set when the sqlite3_step(stmt) has been called but no result has been read */
138+
int pending_step_return_code;
136139
};
137140

138141
static inline php_sqlite3_stmt *php_sqlite3_stmt_from_obj(zend_object *obj) {

ext/sqlite3/sqlite3.c

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ PHP_METHOD(SQLite3, prepare)
517517
object_init_ex(return_value, php_sqlite3_stmt_entry);
518518
stmt_obj = Z_SQLITE3_STMT_P(return_value);
519519
stmt_obj->db_obj = db_obj;
520+
stmt_obj->pending_step_return_code = -1;
520521
ZVAL_OBJ_COPY(&stmt_obj->db_obj_zval, Z_OBJ_P(object));
521522

522523
errcode = sqlite3_prepare_v2(db_obj->db, ZSTR_VAL(sql), ZSTR_LEN(sql), &(stmt_obj->stmt), NULL);
@@ -571,6 +572,7 @@ PHP_METHOD(SQLite3, query)
571572
object_init_ex(&stmt, php_sqlite3_stmt_entry);
572573
stmt_obj = Z_SQLITE3_STMT_P(&stmt);
573574
stmt_obj->db_obj = db_obj;
575+
stmt_obj->pending_step_return_code = -1;
574576
ZVAL_OBJ_COPY(&stmt_obj->db_obj_zval, Z_OBJ_P(object));
575577

576578
return_code = sqlite3_prepare_v2(db_obj->db, ZSTR_VAL(sql), ZSTR_LEN(sql), &(stmt_obj->stmt), NULL);
@@ -591,17 +593,19 @@ PHP_METHOD(SQLite3, query)
591593
ZVAL_OBJ(&result->stmt_obj_zval, Z_OBJ(stmt));
592594

593595
return_code = sqlite3_step(result->stmt_obj->stmt);
596+
stmt_obj->pending_step_return_code = return_code;
594597

595598
switch (return_code) {
596-
case SQLITE_ROW: /* Valid Row */
597599
case SQLITE_DONE: /* Valid but no results */
600+
sqlite3_reset(stmt_obj->stmt);
601+
ZEND_FALLTHROUGH;
602+
case SQLITE_ROW: /* Valid Row */
598603
{
599604
php_sqlite3_free_list *free_item;
600605
free_item = emalloc(sizeof(php_sqlite3_free_list));
601606
free_item->stmt_obj = stmt_obj;
602607
free_item->stmt_obj_zval = stmt;
603608
zend_llist_add_element(&(db_obj->free_list), &free_item);
604-
sqlite3_reset(result->stmt_obj->stmt);
605609
break;
606610
}
607611
default:
@@ -1436,6 +1440,9 @@ PHP_METHOD(SQLite3Stmt, reset)
14361440
php_sqlite3_error(stmt_obj->db_obj, "Unable to reset statement: %s", sqlite3_errmsg(sqlite3_db_handle(stmt_obj->stmt)));
14371441
RETURN_FALSE;
14381442
}
1443+
1444+
stmt_obj->pending_step_return_code = -1;
1445+
14391446
RETURN_TRUE;
14401447
}
14411448
/* }}} */
@@ -1457,6 +1464,8 @@ PHP_METHOD(SQLite3Stmt, clear)
14571464
RETURN_FALSE;
14581465
}
14591466

1467+
stmt_obj->pending_step_return_code = -1;
1468+
14601469
if (stmt_obj->bound_params) {
14611470
zend_hash_destroy(stmt_obj->bound_params);
14621471
FREE_HASHTABLE(stmt_obj->bound_params);
@@ -1787,7 +1796,6 @@ PHP_METHOD(SQLite3Stmt, execute)
17871796
case SQLITE_ROW: /* Valid Row */
17881797
case SQLITE_DONE: /* Valid but no results */
17891798
{
1790-
sqlite3_reset(stmt_obj->stmt);
17911799
object_init_ex(return_value, php_sqlite3_result_entry);
17921800
result = Z_SQLITE3_RESULT_P(return_value);
17931801

@@ -1796,6 +1804,7 @@ PHP_METHOD(SQLite3Stmt, execute)
17961804
result->stmt_obj = stmt_obj;
17971805
result->column_names = NULL;
17981806
result->column_count = -1;
1807+
stmt_obj->pending_step_return_code = return_code;
17991808
ZVAL_OBJ_COPY(&result->stmt_obj_zval, Z_OBJ_P(object));
18001809

18011810
break;
@@ -1941,7 +1950,16 @@ PHP_METHOD(SQLite3Result, fetchArray)
19411950

19421951
SQLITE3_CHECK_INITIALIZED(result_obj->db_obj, result_obj->stmt_obj->initialised, SQLite3Result)
19431952

1944-
ret = sqlite3_step(result_obj->stmt_obj->stmt);
1953+
if (result_obj->stmt_obj->pending_step_return_code != -1) {
1954+
// The SQLite3Result object has just been initialised by SQLite3::query
1955+
// or SQLite3Stmt::execute, and the first step has already been run.
1956+
// For this iteration of fetchArray, skip calling sqlite3_step.
1957+
ret = result_obj->stmt_obj->pending_step_return_code;
1958+
} else {
1959+
ret = sqlite3_step(result_obj->stmt_obj->stmt);
1960+
}
1961+
result_obj->stmt_obj->pending_step_return_code = -1;
1962+
19451963
switch (ret) {
19461964
case SQLITE_ROW:
19471965
/* If there was no return value then just skip fetching */
@@ -2020,6 +2038,7 @@ PHP_METHOD(SQLite3Result, reset)
20202038
SQLITE3_CHECK_INITIALIZED(result_obj->db_obj, result_obj->stmt_obj->initialised, SQLite3Result)
20212039

20222040
sqlite3result_clear_column_names_cache(result_obj);
2041+
result_obj->stmt_obj->pending_step_return_code = -1;
20232042

20242043
if (sqlite3_reset(result_obj->stmt_obj->stmt) != SQLITE_OK) {
20252044
RETURN_FALSE;
@@ -2041,6 +2060,7 @@ PHP_METHOD(SQLite3Result, finalize)
20412060
SQLITE3_CHECK_INITIALIZED(result_obj->db_obj, result_obj->stmt_obj->initialised, SQLite3Result)
20422061

20432062
sqlite3result_clear_column_names_cache(result_obj);
2063+
result_obj->stmt_obj->pending_step_return_code = -1;
20442064

20452065
/* We need to finalize an internal statement */
20462066
if (result_obj->is_prepared_statement == 0) {
@@ -2271,10 +2291,6 @@ static void php_sqlite3_result_object_free_storage(zend_object *object) /* {{{ *
22712291
sqlite3result_clear_column_names_cache(intern);
22722292

22732293
if (!Z_ISNULL(intern->stmt_obj_zval)) {
2274-
if (intern->stmt_obj && intern->stmt_obj->initialised) {
2275-
sqlite3_reset(intern->stmt_obj->stmt);
2276-
}
2277-
22782294
zval_ptr_dtor(&intern->stmt_obj_zval);
22792295
}
22802296

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
--TEST--
2+
Bug 64531 - SQLite3Result::fetchArray executes the query again
3+
--EXTENSIONS--
4+
sqlite3
5+
--FILE--
6+
<?php
7+
8+
require_once(__DIR__ . '/new_db.inc');
9+
10+
// Register two SQLite functions which call back to PHP -
11+
echo "Register call counter function\n";
12+
13+
// "RECOGNISE(x)" returns x and increments a global counter of times x has been
14+
// seen by the function.
15+
function recognise(mixed $value) : mixed
16+
{
17+
$GLOBALS['seen'][$value] = ($GLOBALS['seen'][$value] ?? 0) + 1;
18+
return $value;
19+
};
20+
21+
// "RECOGNISED(x)" returns how many times x has been seen by RECOGNISE.
22+
function recognised(mixed $value) : mixed
23+
{
24+
return $GLOBALS['seen'][$value];
25+
}
26+
27+
$db->createFunction('RECOGNISE', 'recognise');
28+
$db->createFunction('RECOGNISED', 'recognised');
29+
30+
echo "Create table\n\n";
31+
// RECOGNISED(RECOGNISE(x)) should just return 1, 2, 3 for x, x, x if
32+
// everything is going as expected, or 1, 1, 1 for x, y, z.
33+
$db->exec('CREATE TABLE letters (id INTEGER PRIMARY KEY, letter STRING, recognised_on_insert INTEGER)');
34+
35+
echo "Testing SQLite3Result behaviour on INSERT (no result set) when created using SQLite3Stmt->execute\n";
36+
// The RECOGNISE function is called to generate the data before it is written,
37+
// so this should look like VALUES ('a', 1), ('b', 1).
38+
$stmt = $db->prepare('INSERT INTO letters (letter, recognised_on_insert) VALUES (:letter, RECOGNISED(RECOGNISE(:letter)))');
39+
40+
function checkInsertResult(SQLite3Result $result, $letter)
41+
{
42+
printf("'%s' is inserted and has been seen %d times so far.\n", $letter, recognised($letter));
43+
printf("calling fetchArray on the INSERT statement result.\n");
44+
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
45+
printf("unexpected: a result came back.\n");
46+
var_dump($row);
47+
}
48+
$check = recognised($letter);
49+
printf(
50+
"After calling fetch, '%s' has been seen %d times - %s.\n",
51+
$letter, $check, $check === 1 ? "OK" : "NOT OK"
52+
);
53+
printf("calling fetchArray again - this will re-execute (see 79293).\n");
54+
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
55+
printf("unexpected: a result came back.\n");
56+
var_dump($row);
57+
}
58+
$check = recognised($letter);
59+
printf(
60+
"After calling fetch, '%s' has been seen %d times - %s.\n",
61+
$letter, $check, $check === 2 ? "OK" : "NOT OK"
62+
);
63+
}
64+
65+
function checkSelectResult(SQLite3Result $result)
66+
{
67+
echo "Query executed, doing fetch\n";
68+
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
69+
printf(
70+
"letter '%s' was inserted with ID %2.d which has been seen %d times. The letter has now been seen %d times.\n",
71+
$row['letter'],
72+
$row['id'],
73+
$row['id_recognised'],
74+
$row['letter_recognised']
75+
);
76+
}
77+
}
78+
79+
foreach (str_split("abc") as $letter) {
80+
$stmt->bindValue(":letter", $letter);
81+
$result = $stmt->execute();
82+
checkInsertResult($result, $letter);
83+
}
84+
85+
echo "\nTesting SQLite3Result behaviour on INSERT (no result set) when created using SQLite3->query\n";
86+
foreach (str_split("ABC") as $letter) {
87+
$result = $db->query("INSERT INTO letters (letter, recognised_on_insert) VALUES ('$letter', RECOGNISED(RECOGNISE('$letter')))");
88+
checkInsertResult($result, $letter);
89+
}
90+
91+
// Because we've run insert twice per letter above due to 79293, we now expect
92+
// the DB to have (a, a, b, b, c, c, A, A, B, B, C, C). Let's check!
93+
echo "\nTesting SQLite3Result behaviour on SELECT when created using SQLite3Stmt->execute\n";
94+
$stmt = $db->prepare(<<<SQL
95+
SELECT
96+
id,
97+
RECOGNISE(letter) as letter,
98+
RECOGNISED(id) AS id_recognised,
99+
RECOGNISED(letter) as letter_recognised
100+
FROM letters
101+
WHERE id = RECOGNISE(?)
102+
SQL);
103+
for ($i = 1; $i <= 6; $i++) {
104+
$stmt->bindValue(1, $i);
105+
$result = $stmt->execute();
106+
checkSelectResult($result);
107+
}
108+
109+
echo "\nTesting SQLite3Result behaviour on SELECT when created using SQLite3->query\n";
110+
for ($i = 7; $i <= 12; $i++) {
111+
$result = $db->query(<<<"SQL"
112+
SELECT
113+
id,
114+
RECOGNISE(letter) as letter,
115+
RECOGNISED(id) AS id_recognised,
116+
RECOGNISED(letter) as letter_recognised
117+
FROM letters
118+
WHERE id = RECOGNISE($i)
119+
SQL);
120+
checkSelectResult($result);
121+
}
122+
123+
echo "Final result\n\n";
124+
125+
foreach ($GLOBALS['seen'] as $key => $value) {
126+
printf("%s | %3.d\n", $key, $value);
127+
}
128+
129+
echo "Closing database\n";
130+
131+
var_dump($db->close());
132+
echo "Done\n";
133+
?>
134+
--EXPECT--
135+
Register call counter function
136+
Create table
137+
138+
Testing SQLite3Result behaviour on INSERT (no result set) when created using SQLite3Stmt->execute
139+
'a' is inserted and has been seen 1 times so far.
140+
calling fetchArray on the INSERT statement result.
141+
After calling fetch, 'a' has been seen 1 times - OK.
142+
calling fetchArray again - this will re-execute (see 79293).
143+
After calling fetch, 'a' has been seen 2 times - OK.
144+
'b' is inserted and has been seen 1 times so far.
145+
calling fetchArray on the INSERT statement result.
146+
After calling fetch, 'b' has been seen 1 times - OK.
147+
calling fetchArray again - this will re-execute (see 79293).
148+
After calling fetch, 'b' has been seen 2 times - OK.
149+
'c' is inserted and has been seen 1 times so far.
150+
calling fetchArray on the INSERT statement result.
151+
After calling fetch, 'c' has been seen 1 times - OK.
152+
calling fetchArray again - this will re-execute (see 79293).
153+
After calling fetch, 'c' has been seen 2 times - OK.
154+
155+
Testing SQLite3Result behaviour on INSERT (no result set) when created using SQLite3->query
156+
'A' is inserted and has been seen 1 times so far.
157+
calling fetchArray on the INSERT statement result.
158+
After calling fetch, 'A' has been seen 1 times - OK.
159+
calling fetchArray again - this will re-execute (see 79293).
160+
After calling fetch, 'A' has been seen 2 times - OK.
161+
'B' is inserted and has been seen 1 times so far.
162+
calling fetchArray on the INSERT statement result.
163+
After calling fetch, 'B' has been seen 1 times - OK.
164+
calling fetchArray again - this will re-execute (see 79293).
165+
After calling fetch, 'B' has been seen 2 times - OK.
166+
'C' is inserted and has been seen 1 times so far.
167+
calling fetchArray on the INSERT statement result.
168+
After calling fetch, 'C' has been seen 1 times - OK.
169+
calling fetchArray again - this will re-execute (see 79293).
170+
After calling fetch, 'C' has been seen 2 times - OK.
171+
172+
Testing SQLite3Result behaviour on SELECT when created using SQLite3Stmt->execute
173+
Query executed, doing fetch
174+
letter 'a' was inserted with ID 1 which has been seen 1 times. The letter has now been seen 3 times.
175+
Query executed, doing fetch
176+
letter 'a' was inserted with ID 2 which has been seen 1 times. The letter has now been seen 4 times.
177+
Query executed, doing fetch
178+
letter 'b' was inserted with ID 3 which has been seen 1 times. The letter has now been seen 3 times.
179+
Query executed, doing fetch
180+
letter 'b' was inserted with ID 4 which has been seen 1 times. The letter has now been seen 4 times.
181+
Query executed, doing fetch
182+
letter 'c' was inserted with ID 5 which has been seen 1 times. The letter has now been seen 3 times.
183+
Query executed, doing fetch
184+
letter 'c' was inserted with ID 6 which has been seen 1 times. The letter has now been seen 4 times.
185+
186+
Testing SQLite3Result behaviour on SELECT when created using SQLite3->query
187+
Query executed, doing fetch
188+
letter 'A' was inserted with ID 7 which has been seen 1 times. The letter has now been seen 3 times.
189+
Query executed, doing fetch
190+
letter 'A' was inserted with ID 8 which has been seen 1 times. The letter has now been seen 4 times.
191+
Query executed, doing fetch
192+
letter 'B' was inserted with ID 9 which has been seen 1 times. The letter has now been seen 3 times.
193+
Query executed, doing fetch
194+
letter 'B' was inserted with ID 10 which has been seen 1 times. The letter has now been seen 4 times.
195+
Query executed, doing fetch
196+
letter 'C' was inserted with ID 11 which has been seen 1 times. The letter has now been seen 3 times.
197+
Query executed, doing fetch
198+
letter 'C' was inserted with ID 12 which has been seen 1 times. The letter has now been seen 4 times.
199+
Final result
200+
201+
a | 4
202+
b | 4
203+
c | 4
204+
A | 4
205+
B | 4
206+
C | 4
207+
1 | 1
208+
2 | 1
209+
3 | 1
210+
4 | 1
211+
5 | 1
212+
6 | 1
213+
7 | 1
214+
8 | 1
215+
9 | 1
216+
10 | 1
217+
11 | 1
218+
12 | 1
219+
Closing database
220+
bool(true)
221+
Done

0 commit comments

Comments
 (0)