Skip to content

Commit caa7100

Browse files
committed
Rewrite PDO result binding
Instead of requiring the type to be determined in advance by the describer function and then requiring get_col to return a buffer of appropriate type, allow get_col to return an arbitrary zval. See UPGRADING.INTERNALS for a more detailed description of the change. This makes the result fetching simpler, more efficient and more flexible. The general possibility already existed via the special PDO_PARAM_ZVAL type, but the usage was very inconvenient and/or inefficient. Now it's possible to easily implement behavior like "return int if it fits, otherwise string" and to avoid any kind of complex management of temporary buffers. This also fixes bug #40913 (our second highest voted bug of all time, for some reason). PARAM_LOB result bindings will now consistently return a stream resource, independently of the used database driver. I've tried my best to update all PDO drivers for this change, but some of the changes may be broken, as I cannot test or even build some of these drivers (in particular PDO dblib and PDO oci). Fixes are appreciated -- a working CI setup would be even more appreciated ;)
1 parent 57d69b5 commit caa7100

22 files changed

+347
-584
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ PHP NEWS
2121
- OpenSSL:
2222
. Bump minimal OpenSSL version to 1.0.2. (Jakub Zelenka)
2323

24+
- PDO:
25+
. Fixed bug #40913 (PDO_MYSQL: PDO::PARAM_LOB does not bind to a stream for
26+
fetching a BLOB). (Nikita)
27+
2428
- PSpell:
2529
. Convert resource<pspell> to object \PSpell. (Sara)
2630
. Convert resource<pspell config> to object \PSPellConfig. (Sara)

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ PHP 8.1 UPGRADE NOTES
4242
- PDO:
4343
. PDO::ATTR_STRINGIFY_FETCHES now also stringifies values of type bool to
4444
"0" or "1". Previously booleans were not stringified.
45+
. Calling bindColumn() with PDO::PARAM_LOB (and assuming stringification is
46+
not enabled) will now consistently bind a stream result, as documented.
47+
Previously the result would be either a stream or a string depending on the
48+
used database driver and the time the binding is performed.
4549

4650
- PDO MySQL:
4751
. Integers and floats in result sets will now be returned using native PHP

UPGRADING.INTERNALS

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,25 @@ PHP 8.1 INTERNALS UPGRADE NOTES
2525
configuration. If the an algorithm doesn't make use of any
2626
additional configuration, the argument is to be marked with
2727
ZEND_ATTRIBUTE_UNUSED.
28+
29+
b. ext/pdo
30+
- The "preparer" callback now accepts a zend_string* instead of
31+
char* + size_t pair the query string. Similarly, the query_string and
32+
active_query_string members of pdo_stmt_t are now zend_string*.
33+
- The way in which drivers provide results has changed: Previously,
34+
the "describer" callback populated the "pdo_type" member in the
35+
pdo_column_data structure, and the "get_col" callback then had to return
36+
pointers to data of appropriate type.
37+
38+
In PHP 8.1, the "describer" callback no longer determines the pdo_type
39+
(and this member has been removed from pdo_column_data). Instead, the
40+
"get_col" callback accepts a zval pointer that may be populated with a
41+
value of arbitrary type. This gives drivers more flexibility in
42+
determining result types (e.g. based on whether a specific integer fits
43+
the PHP integer type) and avoid awkward juggling of temporary buffers.
44+
45+
As the "describer" no longer determines pdo_type, the "get_column_meta"
46+
function is now responsible for providing this information for use by
47+
getColumnMeta(). The type provided here does not need to match the type
48+
returned by get_col (in fact no corresponding type might exist, e.g. for
49+
floats). It should be the closest logical equivalent for the column type.

ext/pdo/pdo_stmt.c

Lines changed: 44 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,8 @@ PHP_METHOD(PDOStatement, execute)
486486
}
487487
/* }}} */
488488

489-
static inline void fetch_value(pdo_stmt_t *stmt, zval *dest, int colno, int *type_override) /* {{{ */
489+
static inline void fetch_value(pdo_stmt_t *stmt, zval *dest, int colno, enum pdo_param_type *type_override) /* {{{ */
490490
{
491-
struct pdo_column_data *col;
492-
char *value = NULL;
493-
size_t value_len = 0;
494-
int caller_frees = 0;
495-
int type, new_type;
496-
497491
if (colno < 0) {
498492
zend_value_error("Column index must be greater than or equal to 0");
499493
ZVAL_NULL(dest);
@@ -506,126 +500,62 @@ static inline void fetch_value(pdo_stmt_t *stmt, zval *dest, int colno, int *typ
506500
return;
507501
}
508502

509-
col = &stmt->columns[colno];
510-
type = PDO_PARAM_TYPE(col->param_type);
511-
new_type = type_override ? (int)PDO_PARAM_TYPE(*type_override) : type;
512-
513-
value = NULL;
514-
value_len = 0;
503+
ZVAL_NULL(dest);
504+
stmt->methods->get_col(stmt, colno, dest, type_override);
515505

516-
stmt->methods->get_col(stmt, colno, &value, &value_len, &caller_frees);
517-
518-
switch (type) {
519-
case PDO_PARAM_ZVAL:
520-
if (value && value_len == sizeof(zval)) {
521-
ZVAL_COPY_VALUE(dest, (zval *)value);
522-
523-
if (Z_TYPE_P(dest) == IS_STRING && Z_STRLEN_P(dest) == 0
524-
&& stmt->dbh->oracle_nulls == PDO_NULL_EMPTY_STRING) {
525-
zval_ptr_dtor_str(dest);
526-
ZVAL_NULL(dest);
527-
}
528-
} else {
529-
ZVAL_NULL(dest);
530-
}
506+
if (Z_TYPE_P(dest) == IS_STRING && Z_STRLEN_P(dest) == 0
507+
&& stmt->dbh->oracle_nulls == PDO_NULL_EMPTY_STRING) {
508+
zval_ptr_dtor_str(dest);
509+
ZVAL_NULL(dest);
510+
}
531511

532-
if (Z_TYPE_P(dest) == IS_NULL) {
533-
type = new_type;
534-
}
535-
break;
512+
/* If stringification is requested, override with PDO_PARAM_STR. */
513+
enum pdo_param_type pdo_param_str = PDO_PARAM_STR;
514+
if (stmt->dbh->stringify) {
515+
type_override = &pdo_param_str;
516+
}
536517

537-
case PDO_PARAM_INT:
538-
if (value && value_len == sizeof(zend_long)) {
539-
ZVAL_LONG(dest, *(zend_long*)value);
518+
if (type_override && Z_TYPE_P(dest) != IS_NULL) {
519+
switch (*type_override) {
520+
case PDO_PARAM_INT:
521+
convert_to_long(dest);
540522
break;
541-
}
542-
ZVAL_NULL(dest);
543-
break;
544-
545-
case PDO_PARAM_BOOL:
546-
if (value && value_len == sizeof(zend_bool)) {
547-
ZVAL_BOOL(dest, *(zend_bool*)value);
523+
case PDO_PARAM_BOOL:
524+
convert_to_boolean(dest);
548525
break;
549-
}
550-
ZVAL_NULL(dest);
551-
break;
552-
553-
case PDO_PARAM_LOB:
554-
if (value == NULL) {
555-
ZVAL_NULL(dest);
556-
} else if (value_len == 0) {
557-
/* Warning, empty strings need to be passed as stream */
558-
if (stmt->dbh->stringify || new_type == PDO_PARAM_STR) {
559-
zend_string *buf;
560-
buf = php_stream_copy_to_mem((php_stream*)value, PHP_STREAM_COPY_ALL, 0);
561-
if (buf == NULL) {
526+
case PDO_PARAM_STR:
527+
if (Z_TYPE_P(dest) == IS_FALSE) {
528+
/* Return "0" rather than "", because this is what database drivers that
529+
* don't have a dedicated boolean type would return. */
530+
zval_ptr_dtor_nogc(dest);
531+
ZVAL_INTERNED_STR(dest, ZSTR_CHAR('0'));
532+
} else if (Z_TYPE_P(dest) == IS_RESOURCE) {
533+
/* Convert LOB stream to string */
534+
php_stream *stream;
535+
php_stream_from_zval_no_verify(stream, dest);
536+
zend_string *str = php_stream_copy_to_mem(stream, PHP_STREAM_COPY_ALL, 0);
537+
zval_ptr_dtor_nogc(dest);
538+
if (str == NULL) {
562539
ZVAL_EMPTY_STRING(dest);
563540
} else {
564-
ZVAL_STR(dest, buf);
541+
ZVAL_STR(dest, str);
565542
}
566-
php_stream_close((php_stream*)value);
567-
} else {
568-
php_stream_to_zval((php_stream*)value, dest);
569-
}
570-
} else if (!stmt->dbh->stringify && new_type != PDO_PARAM_STR) {
571-
/* they gave us a string, but LOBs are represented as streams in PDO */
572-
zend_string *str = zend_string_init(value, value_len, 0);
573-
php_stream *stream = php_stream_memory_open(TEMP_STREAM_READONLY, str);
574-
if (stream) {
575-
php_stream_to_zval(stream, dest);
576543
} else {
577-
ZVAL_NULL(dest);
544+
convert_to_string(dest);
578545
}
579-
zend_string_release(str);
580-
} else {
581-
ZVAL_STRINGL(dest, value, value_len);
582-
}
583-
break;
584-
585-
case PDO_PARAM_STR:
586-
if (value && !(value_len == 0 && stmt->dbh->oracle_nulls == PDO_NULL_EMPTY_STRING)) {
587-
ZVAL_STRINGL(dest, value, value_len);
588-
break;
589-
}
590-
default:
591-
ZVAL_NULL(dest);
592-
}
593-
594-
if (type != new_type) {
595-
switch (new_type) {
596-
case PDO_PARAM_INT:
597-
convert_to_long_ex(dest);
598-
break;
599-
case PDO_PARAM_BOOL:
600-
convert_to_boolean_ex(dest);
601-
break;
602-
case PDO_PARAM_STR:
603-
convert_to_string_ex(dest);
604546
break;
605547
case PDO_PARAM_NULL:
606-
convert_to_null_ex(dest);
548+
convert_to_null(dest);
607549
break;
608-
default:
609-
;
610-
}
611-
}
612-
613-
if (caller_frees && value) {
614-
efree(value);
615-
}
616-
617-
if (stmt->dbh->stringify) {
618-
switch (Z_TYPE_P(dest)) {
619-
case IS_FALSE:
620-
/* Return "0" rather than "", because this is what database drivers that
621-
* don't have a dedicated boolean type would return. */
622-
zval_ptr_dtor_nogc(dest);
623-
ZVAL_INTERNED_STR(dest, ZSTR_CHAR('0'));
550+
case PDO_PARAM_LOB:
551+
if (Z_TYPE_P(dest) == IS_STRING) {
552+
php_stream *stream =
553+
php_stream_memory_open(TEMP_STREAM_READONLY, Z_STR_P(dest));
554+
zval_ptr_dtor_str(dest);
555+
php_stream_to_zval(stream, dest);
556+
}
624557
break;
625-
case IS_TRUE:
626-
case IS_LONG:
627-
case IS_DOUBLE:
628-
convert_to_string(dest);
558+
default:
629559
break;
630560
}
631561
}
@@ -673,7 +603,7 @@ static bool do_fetch_common(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, ze
673603
zval_ptr_dtor(Z_REFVAL(param->parameter));
674604

675605
/* set new value */
676-
fetch_value(stmt, Z_REFVAL(param->parameter), param->paramno, (int *)&param->param_type);
606+
fetch_value(stmt, Z_REFVAL(param->parameter), param->paramno, &param->param_type);
677607

678608
/* TODO: some smart thing that avoids duplicating the value in the
679609
* general loop below. For now, if you're binding output columns,
@@ -1761,10 +1691,6 @@ PHP_METHOD(PDOStatement, getColumnMeta)
17611691
add_assoc_str(return_value, "name", zend_string_copy(col->name));
17621692
add_assoc_long(return_value, "len", col->maxlen); /* FIXME: unsigned ? */
17631693
add_assoc_long(return_value, "precision", col->precision);
1764-
if (col->param_type != PDO_PARAM_ZVAL) {
1765-
/* if param_type is PDO_PARAM_ZVAL the driver has to provide correct data */
1766-
add_assoc_long(return_value, "pdo_type", col->param_type);
1767-
}
17681694
}
17691695
/* }}} */
17701696

ext/pdo/php_pdo_driver.h

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,14 @@ PDO_API char *php_pdo_int64_to_str(pdo_int64_t i64);
4646

4747
enum pdo_param_type {
4848
PDO_PARAM_NULL,
49-
50-
/* int as in long (the php native int type).
51-
* If you mark a column as an int, PDO expects get_col to return
52-
* a pointer to a long */
49+
PDO_PARAM_BOOL,
5350
PDO_PARAM_INT,
54-
55-
/* get_col ptr should point to start of the string buffer */
5651
PDO_PARAM_STR,
57-
58-
/* get_col: when len is 0 ptr should point to a php_stream *,
59-
* otherwise it should behave like a string. Indicate a NULL field
60-
* value by setting the ptr to NULL */
6152
PDO_PARAM_LOB,
6253

63-
/* get_col: will expect the ptr to point to a new PDOStatement object handle,
64-
* but this isn't wired up yet */
54+
/* get_col: Not supported (yet?) */
6555
PDO_PARAM_STMT, /* hierarchical result set */
6656

67-
/* get_col ptr should point to a zend_bool */
68-
PDO_PARAM_BOOL,
69-
70-
/* get_col ptr should point to a zval*
71-
and the driver is responsible for adding correct type information to get_column_meta()
72-
*/
73-
PDO_PARAM_ZVAL,
74-
7557
/* magic flag to denote a parameter as being input/output */
7658
PDO_PARAM_INPUT_OUTPUT = 0x80000000,
7759

@@ -343,13 +325,13 @@ typedef int (*pdo_stmt_fetch_func)(pdo_stmt_t *stmt,
343325
* Driver should populate stmt->columns[colno] with appropriate info */
344326
typedef int (*pdo_stmt_describe_col_func)(pdo_stmt_t *stmt, int colno);
345327

346-
/* retrieves pointer and size of the value for a column.
347-
* Note that PDO expects the driver to manage the lifetime of this data;
348-
* it will copy the value into a zval on behalf of the script.
349-
* If the driver sets caller_frees, ptr should point to emalloc'd memory
350-
* and PDO will free it as soon as it is done using it.
351-
*/
352-
typedef int (*pdo_stmt_get_col_data_func)(pdo_stmt_t *stmt, int colno, char **ptr, size_t *len, int *caller_frees);
328+
/* Retrieves zval value of a column. If type is non-NULL, then this specifies the type which
329+
* the user requested through bindColumn(). The driver does not need to check this argument,
330+
* as PDO will perform any necessary conversions itself. However, it might be used to fetch
331+
* a value more efficiently into the final type, or make the behavior dependent on the requested
332+
* type. */
333+
typedef int (*pdo_stmt_get_col_data_func)(
334+
pdo_stmt_t *stmt, int colno, zval *result, enum pdo_param_type *type);
353335

354336
/* hook for bound params */
355337
enum pdo_param_event {
@@ -382,8 +364,8 @@ typedef int (*pdo_stmt_get_attr_func)(pdo_stmt_t *stmt, zend_long attr, zval *va
382364
* name => the column name
383365
* len => the length/size of the column
384366
* precision => precision of the column
385-
* pdo_type => an integer, one of the PDO_PARAM_XXX values
386367
*
368+
* pdo_type => an integer, one of the PDO_PARAM_XXX values
387369
* scale => the floating point scale
388370
* table => the table for that column
389371
* type => a string representation of the type, mapped to the PHP equivalent type name
@@ -541,7 +523,6 @@ struct pdo_column_data {
541523
zend_string *name;
542524
size_t maxlen;
543525
zend_ulong precision;
544-
enum pdo_param_type param_type;
545526
};
546527

547528
/* describes a bound parameter */

ext/pdo/tests/debug_emulated_prepares.phpt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ Key: Name: [5] :bool
4747
paramno=-1
4848
name=[5] ":bool"
4949
is_param=1
50-
param_type=5
50+
param_type=1
5151
Key: Name: [4] :int
5252
paramno=-1
5353
name=[4] ":int"
5454
is_param=1
55-
param_type=1
55+
param_type=2
5656
Key: Name: [7] :string
5757
paramno=-1
5858
name=[7] ":string"
5959
is_param=1
60-
param_type=2
60+
param_type=3
6161
Key: Name: [5] :null
6262
paramno=-1
6363
name=[5] ":null"

0 commit comments

Comments
 (0)