Skip to content

Commit 0d584c3

Browse files
pdo_odbc: Don't fetch 256 byte blocks for long columns (#10809)
* pdo_odbc: Don't fetch 256 byte blocks for long columns Fetching 256 byte blocks can confuse some drivers with conversion routines. That, and it seems to me the round trips to and from a database could be a major performance impact. Instead, we try to fetch all at once, and continue fetching if a driver somehow has more for us. This has been tested with a problematic case with the Db2i driver with stateful MBCS encodings. See GH-10733 for discussion about this and issues it can resolve. * change to separate by 256 bytes, when C->fetched_len == SQL_NO_TOTAL change to separate by 256 bytes, when C->fetched_len == SQL_NO_TOTAL changed from 256 byte to 2048 byte buf block. * Make long column buffer size single define Could be configurable maybe, but best to avoid magic numbers even for a compile-time constant. * Use ZendMM page size minus zend_string overhead Change recommended by Christoph. Probably a little better performance wise I have to guess. * [skip ci] Update comment to mention constant * Update UPGRADING for PDO_ODBC change mention GH issues in UPGRADING too * Update NEWS for PDO_ODBC change --------- Co-authored-by: SakiTakamachi <[email protected]>
1 parent cea0918 commit 0d584c3

File tree

3 files changed

+59
-25
lines changed

3 files changed

+59
-25
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.5.0alpha2
44

5+
- PDO_ODBC
6+
. Fetch larger block sizes and better handle SQL_NO_TOTAL when calling
7+
SQLGetData. (Calvin Buckley, Saki Takamachi)
8+
59
- Standard:
610
. Optimized pack(). (nielsdos, divinity76)
711

UPGRADING

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,13 @@ PHP 8.5 UPGRADE NOTES
599599
. The `-z` or `--zend-extension` option has been removed as it was
600600
non-functional. Use `-d zend_extension=<path>` instead.
601601

602+
- PDO_ODBC
603+
. The fetch behaviour for larger columns has been changed. Rather than
604+
fetching 256 byte blocks, PDO_ODBC will try to fetch a larger block size;
605+
currently, this is the page size minus string overhead. Drivers that
606+
return SQL_NO_TOTAL in SQLGetData are also better handled as well.
607+
This should improve compatibility and performance. See GH-10809, GH-10733.
608+
602609
========================================
603610
14. Performance Improvements
604611
========================================

ext/pdo_odbc/odbc_stmt.c

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
#include "php_pdo_odbc.h"
2727
#include "php_pdo_odbc_int.h"
2828

29+
/* Buffer size; bigger columns than this become a "long column" */
30+
#define LONG_COLUMN_BUFFER_SIZE (ZEND_MM_PAGE_SIZE- ZSTR_MAX_OVERHEAD)
31+
2932
enum pdo_odbc_conv_result {
3033
PDO_ODBC_CONV_NOT_REQUIRED,
3134
PDO_ODBC_CONV_OK,
@@ -615,7 +618,7 @@ static int odbc_stmt_describe(pdo_stmt_t *stmt, int colno)
615618
/* tell ODBC to put it straight into our buffer, but only if it
616619
* isn't "long" data, and only if we haven't already bound a long
617620
* column. */
618-
if (colsize < 256 && !S->going_long) {
621+
if (colsize < LONG_COLUMN_BUFFER_SIZE && !S->going_long) {
619622
S->cols[colno].data = emalloc(colsize+1);
620623
S->cols[colno].is_long = 0;
621624

@@ -631,7 +634,7 @@ static int odbc_stmt_describe(pdo_stmt_t *stmt, int colno)
631634
} else {
632635
/* allocate a smaller buffer to keep around for smaller
633636
* "long" columns */
634-
S->cols[colno].data = emalloc(256);
637+
S->cols[colno].data = emalloc(LONG_COLUMN_BUFFER_SIZE);
635638
S->going_long = 1;
636639
S->cols[colno].is_long = 1;
637640
}
@@ -657,37 +660,57 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo
657660
RETCODE rc;
658661

659662
/* fetch it into C->data, which is allocated with a length
660-
* of 256 bytes; if there is more to be had, we then allocate
663+
* of the page size minus zend_string overhead (LONG_COLUMN_BUFFER_SIZE);
664+
* if there is more to be had, we then allocate
661665
* bigger buffer for the caller to free */
662666

663667
rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, C->data,
664-
256, &C->fetched_len);
668+
LONG_COLUMN_BUFFER_SIZE, &C->fetched_len);
665669
orig_fetched_len = C->fetched_len;
666670

667-
if (rc == SQL_SUCCESS && C->fetched_len < 256) {
671+
if (rc == SQL_SUCCESS && C->fetched_len < LONG_COLUMN_BUFFER_SIZE) {
668672
/* all the data fit into our little buffer;
669673
* jump down to the generic bound data case */
670674
goto in_data;
671675
}
672676

673677
if (rc == SQL_SUCCESS_WITH_INFO || rc == SQL_SUCCESS) {
674-
/* this is a 'long column'
675-
676-
read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks
677-
in order into the output buffer; 255 bytes are an optimistic assumption, since the driver may assert
678-
more or less NUL bytes at the end; we cater to that later, if actual length information is available
679-
680-
this loop has to work whether or not SQLGetData() provides the total column length.
681-
calling SQLDescribeCol() or other, specifically to get the column length, then doing a single read
682-
for that size would be slower except maybe for extremely long columns.*/
683-
char *buf2 = emalloc(256);
684-
zend_string *str = zend_string_init(C->data, 256, 0);
685-
size_t used = 255; /* not 256; the driver NUL terminated the buffer */
678+
/*
679+
* This is a long column.
680+
*
681+
* Try to get as much as we can at once. If the
682+
* driver somehow has more for us, get more. We'll
683+
* assemble it into one big buffer at the end.
684+
*
685+
* N.B. with n and n+1 mentioned in the comments:
686+
* n is the size returned without null terminator.
687+
*
688+
* The extension previously tried getting it in 256
689+
* byte blocks, but this could have created trouble
690+
* with some drivers.
691+
*
692+
* However, depending on the driver, fetched_len may
693+
* not contain the number of bytes and SQL_NO_TOTAL
694+
* may be passed.
695+
* The behavior in this case is the same as before,
696+
* dividing the data into blocks. However, it has been
697+
* changed from 256 byte to LONG_COLUMN_BUFFER_SIZE.
698+
*/
699+
ssize_t to_fetch_len;
700+
if (orig_fetched_len == SQL_NO_TOTAL) {
701+
to_fetch_len = C->datalen > (LONG_COLUMN_BUFFER_SIZE - 1) ? (LONG_COLUMN_BUFFER_SIZE - 1) : C->datalen;
702+
} else {
703+
to_fetch_len = orig_fetched_len;
704+
}
705+
ssize_t to_fetch_byte = to_fetch_len + 1;
706+
char *buf2 = emalloc(to_fetch_byte);
707+
zend_string *str = zend_string_init(C->data, to_fetch_byte, 0);
708+
size_t used = to_fetch_len;
686709

687710
do {
688711
C->fetched_len = 0;
689-
/* read block. 256 bytes => 255 bytes are actually read, the last 1 is NULL */
690-
rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, buf2, 256, &C->fetched_len);
712+
/* read block. n + 1 bytes => n bytes are actually read, the last 1 is NULL */
713+
rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, buf2, to_fetch_byte, &C->fetched_len);
691714

692715
/* adjust `used` in case we have proper length info from the driver */
693716
if (orig_fetched_len >= 0 && C->fetched_len >= 0) {
@@ -698,13 +721,13 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo
698721
}
699722

700723
/* resize output buffer and reassemble block */
701-
if (rc==SQL_SUCCESS_WITH_INFO || (rc==SQL_SUCCESS && C->fetched_len > 255)) {
724+
if (rc==SQL_SUCCESS_WITH_INFO || (rc==SQL_SUCCESS && C->fetched_len > to_fetch_len)) {
702725
/* point 5, in section "Retrieving Data with SQLGetData" in http://msdn.microsoft.com/en-us/library/windows/desktop/ms715441(v=vs.85).aspx
703-
states that if SQL_SUCCESS_WITH_INFO, fetched_len will be > 255 (greater than buf2's size)
704-
(if a driver fails to follow that and wrote less than 255 bytes to buf2, this will AV or read garbage into buf) */
705-
str = zend_string_realloc(str, used + 256, 0);
706-
memcpy(ZSTR_VAL(str) + used, buf2, 256);
707-
used = used + 255;
726+
states that if SQL_SUCCESS_WITH_INFO, fetched_len will be > n (greater than buf2's size)
727+
(if a driver fails to follow that and wrote less than n bytes to buf2, this will AV or read garbage into buf) */
728+
str = zend_string_realloc(str, used + to_fetch_byte, 0);
729+
memcpy(ZSTR_VAL(str) + used, buf2, to_fetch_byte);
730+
used = used + to_fetch_len;
708731
} else if (rc==SQL_SUCCESS) {
709732
str = zend_string_realloc(str, used + C->fetched_len, 0);
710733
memcpy(ZSTR_VAL(str) + used, buf2, C->fetched_len);

0 commit comments

Comments
 (0)