Skip to content

pdo_odbc: Don't fetch 256 byte blocks for long columns #10809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.5.0alpha2

- PDO_ODBC
. Fetch larger block sizes and better handle SQL_NO_TOTAL when calling
SQLGetData. (Calvin Buckley, Saki Takamachi)

- Standard:
. Optimized pack(). (nielsdos, divinity76)

Expand Down
7 changes: 7 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,13 @@ PHP 8.5 UPGRADE NOTES
. The `-z` or `--zend-extension` option has been removed as it was
non-functional. Use `-d zend_extension=<path>` instead.

- PDO_ODBC
. The fetch behaviour for larger columns has been changed. Rather than
fetching 256 byte blocks, PDO_ODBC will try to fetch a larger block size;
currently, this is the page size minus string overhead. Drivers that
return SQL_NO_TOTAL in SQLGetData are also better handled as well.
This should improve compatibility and performance. See GH-10809, GH-10733.

========================================
14. Performance Improvements
========================================
Expand Down
73 changes: 48 additions & 25 deletions ext/pdo_odbc/odbc_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
#include "php_pdo_odbc.h"
#include "php_pdo_odbc_int.h"

/* Buffer size; bigger columns than this become a "long column" */
#define LONG_COLUMN_BUFFER_SIZE (ZEND_MM_PAGE_SIZE- ZSTR_MAX_OVERHEAD)

enum pdo_odbc_conv_result {
PDO_ODBC_CONV_NOT_REQUIRED,
PDO_ODBC_CONV_OK,
Expand Down Expand Up @@ -615,7 +618,7 @@ static int odbc_stmt_describe(pdo_stmt_t *stmt, int colno)
/* tell ODBC to put it straight into our buffer, but only if it
* isn't "long" data, and only if we haven't already bound a long
* column. */
if (colsize < 256 && !S->going_long) {
if (colsize < LONG_COLUMN_BUFFER_SIZE && !S->going_long) {
S->cols[colno].data = emalloc(colsize+1);
S->cols[colno].is_long = 0;

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

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

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

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

if (rc == SQL_SUCCESS_WITH_INFO || rc == SQL_SUCCESS) {
/* this is a 'long column'

read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks
in order into the output buffer; 255 bytes are an optimistic assumption, since the driver may assert
more or less NUL bytes at the end; we cater to that later, if actual length information is available

this loop has to work whether or not SQLGetData() provides the total column length.
calling SQLDescribeCol() or other, specifically to get the column length, then doing a single read
for that size would be slower except maybe for extremely long columns.*/
char *buf2 = emalloc(256);
zend_string *str = zend_string_init(C->data, 256, 0);
size_t used = 255; /* not 256; the driver NUL terminated the buffer */
/*
* This is a long column.
*
* Try to get as much as we can at once. If the
* driver somehow has more for us, get more. We'll
* assemble it into one big buffer at the end.
*
* N.B. with n and n+1 mentioned in the comments:
* n is the size returned without null terminator.
*
* The extension previously tried getting it in 256
* byte blocks, but this could have created trouble
* with some drivers.
*
* However, depending on the driver, fetched_len may
* not contain the number of bytes and SQL_NO_TOTAL
* may be passed.
* The behavior in this case is the same as before,
* dividing the data into blocks. However, it has been
* changed from 256 byte to LONG_COLUMN_BUFFER_SIZE.
*/
ssize_t to_fetch_len;
if (orig_fetched_len == SQL_NO_TOTAL) {
to_fetch_len = C->datalen > (LONG_COLUMN_BUFFER_SIZE - 1) ? (LONG_COLUMN_BUFFER_SIZE - 1) : C->datalen;
} else {
to_fetch_len = orig_fetched_len;
}
ssize_t to_fetch_byte = to_fetch_len + 1;
char *buf2 = emalloc(to_fetch_byte);
zend_string *str = zend_string_init(C->data, to_fetch_byte, 0);
size_t used = to_fetch_len;

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

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

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