Simplified get_field_as_string and made it more robust (#1265)

This commit is contained in:
Jenny Tam 2021-06-08 12:37:28 -07:00 committed by GitHub
parent a14cb70ad3
commit af61d06bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 273 additions and 253 deletions

View file

@ -1621,8 +1621,8 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f
*field_len = len;
}
void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type,
_Inout_updates_bytes_(*field_len) void*& field_value, _Inout_ SQLLEN* field_len )
void get_field_as_string(_Inout_ sqlsrv_stmt *stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type,
_Inout_updates_bytes_(*field_len) void *&field_value, _Inout_ SQLLEN *field_len)
{
SQLRETURN r;
SQLSMALLINT c_type;
@ -1631,7 +1631,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
SQLLEN field_len_temp = 0;
SQLLEN sql_display_size = 0;
char* field_value_temp = NULL;
unsigned int intial_field_len = INITIAL_FIELD_STRING_LEN;
unsigned int initial_field_len = INITIAL_FIELD_STRING_LEN;
try {
@ -1670,222 +1670,144 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
if (sqlsrv_php_type.typeinfo.encoding == CP_UTF8 && !is_a_numeric_type(sql_field_type)) {
c_type = SQL_C_WCHAR;
extra = sizeof(SQLWCHAR);
sql_display_size = (sql_display_size * sizeof(SQLWCHAR));
}
}
// If this is a large type, then read the first few bytes to get the actual length from SQLGetData
// If this is a large type, then read the first chunk to get the actual length from SQLGetData
// The user may use "SET TEXTSIZE" to specify the size of varchar(max), nvarchar(max),
// varbinary(max), text, ntext, and image data returned by a SELECT statement.
// For varchar(max) and nvarchar(max), sql_display_size will be 0, regardless
if (sql_display_size == 0 || sql_display_size == INT_MAX ||
sql_display_size == INT_MAX >> 1 || sql_display_size == UINT_MAX - 1 ||
(sql_display_size > SQL_SERVER_MAX_FIELD_SIZE &&
(sql_field_type == SQL_WLONGVARCHAR || sql_field_type == SQL_LONGVARCHAR || sql_field_type == SQL_LONGVARBINARY))) {
// For varbinary(max), varchar(max) and nvarchar(max), sql_display_size will be 0, regardless
if (sql_display_size == 0 ||
(sql_field_type == SQL_WLONGVARCHAR || sql_field_type == SQL_LONGVARCHAR || sql_field_type == SQL_LONGVARBINARY)) {
field_len_temp = intial_field_len;
SQLLEN initiallen = field_len_temp + extra;
field_value_temp = static_cast<char*>( sqlsrv_malloc( field_len_temp + extra + 1 ));
r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp, ( field_len_temp + extra ),
&field_len_temp, false /*handle_warning*/ );
CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) {
throw core::CoreException();
}
if( field_len_temp == SQL_NULL_DATA ) {
field_value = NULL;
sqlsrv_free( field_value_temp );
return;
}
if( r == SQL_SUCCESS_WITH_INFO ) {
SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = {L'\0'};
SQLSMALLINT len = 0;
stmt->current_results->get_diag_field( 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len );
// with Linux connection pooling may not get a truncated warning back but the actual field_len_temp
// can be greater than the initallen value.
#ifndef _WIN32
if( is_truncated_warning( state ) || initiallen < field_len_temp) {
#else
if( is_truncated_warning( state ) ) {
#endif // !_WIN32
SQLLEN dummy_field_len = 0;
// for XML (and possibly other conditions) the field length returned is not the real field length, so
// in every pass, we double the allocation size to retrieve all the contents.
if( field_len_temp == SQL_NO_TOTAL ) {
// reset the field_len_temp
field_len_temp = intial_field_len;
do {
SQLLEN initial_field_len = field_len_temp;
// Double the size.
field_len_temp *= 2;
field_value_temp = static_cast<char*>( sqlsrv_realloc( field_value_temp, field_len_temp + extra + 1 ));
field_len_temp -= initial_field_len;
// Get the rest of the data.
r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp + initial_field_len,
field_len_temp + extra, &dummy_field_len, false /*handle_warning*/ );
// the last packet will contain the actual amount retrieved, not SQL_NO_TOTAL
// so we calculate the actual length of the string with that.
if ( dummy_field_len != SQL_NO_TOTAL )
field_len_temp += dummy_field_len;
else
field_len_temp += initial_field_len;
if( r == SQL_SUCCESS_WITH_INFO ) {
core::SQLGetDiagField( stmt, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len
);
}
} while( r == SQL_SUCCESS_WITH_INFO && is_truncated_warning( state ));
}
else {
// the real field length is returned here, thus no need to double the allocation size here, just have to
// allocate field_len_temp (which is the field length retrieved from the first SQLGetData
field_value_temp = static_cast<char*>( sqlsrv_realloc( field_value_temp, field_len_temp + extra + 1 ));
// We have already received intial_field_len size data.
field_len_temp -= intial_field_len;
// Get the rest of the data.
r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp + intial_field_len,
field_len_temp + extra, &dummy_field_len, true /*handle_warning*/ );
field_len_temp += intial_field_len;
if( dummy_field_len == SQL_NULL_DATA ) {
field_value = NULL;
sqlsrv_free( field_value_temp );
return;
}
CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) {
throw core::CoreException();
}
}
} // if( is_truncation_warning ( state ) )
else {
CHECK_SQL_ERROR_OR_WARNING( r, stmt ) {
throw core::CoreException();
}
}
} // if( r == SQL_SUCCESS_WITH_INFO )
if (c_type == SQL_C_WCHAR) {
bool converted = convert_string_from_utf16_inplace( static_cast<SQLSRV_ENCODING>( sqlsrv_php_type.typeinfo.encoding ),
&field_value_temp, field_len_temp );
CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) {
throw core::CoreException();
}
}
} // if ( sql_display_size == 0 || sql_display_size == LONG_MAX .. )
else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE ) {
// only allow binary retrievals for char and binary types. All others get a string converted
// to the encoding type they asked for.
// null terminator
if( c_type == SQL_C_CHAR ) {
sql_display_size += sizeof( SQLCHAR );
}
// For WCHAR multiply by sizeof(WCHAR) and include the null terminator
else if( c_type == SQL_C_WCHAR ) {
sql_display_size = (sql_display_size * sizeof(WCHAR)) + sizeof(WCHAR);
}
field_value_temp = static_cast<char*>( sqlsrv_malloc( sql_display_size + extra + 1 ));
field_len_temp = initial_field_len;
field_value_temp = static_cast<char*>(sqlsrv_malloc(field_len_temp + extra + 1));
r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp, (field_len_temp + extra), &field_len_temp, false /*handle_warning*/);
} else {
field_len_temp = sql_display_size;
field_value_temp = static_cast<char*>(sqlsrv_malloc(sql_display_size + extra + 1));
// get the data
r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp, sql_display_size,
&field_len_temp, true /*handle_warning*/ );
CHECK_SQL_ERROR( r, stmt ) {
throw core::CoreException();
}
CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) {
throw core::CoreException();
}
if( field_len_temp == SQL_NULL_DATA ) {
field_value = NULL;
sqlsrv_free( field_value_temp );
return;
}
if (c_type == SQL_C_WCHAR) {
bool converted = convert_string_from_utf16_inplace( static_cast<SQLSRV_ENCODING>( sqlsrv_php_type.typeinfo.encoding ),
&field_value_temp, field_len_temp );
CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) {
throw core::CoreException();
}
}
if (stmt->format_decimals && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) {
// number of decimal places only affect money / smallmoney fields
SQLSMALLINT decimal_places = (stmt->current_meta_data[field_index]->field_is_money_type) ? stmt->decimal_places : NO_CHANGE_DECIMAL_PLACES;
format_decimal_numbers(decimal_places, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp);
}
} // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE )
else {
DIE( "Invalid sql_display_size" );
return; // to eliminate a warning
r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp, sql_display_size + extra, &field_len_temp, false /*handle_warning*/);
}
field_value = field_value_temp;
*field_len = field_len_temp;
CHECK_CUSTOM_ERROR((r == SQL_NO_DATA), stmt, SQLSRV_ERROR_NO_DATA, field_index) {
throw core::CoreException();
}
if (field_len_temp == SQL_NULL_DATA) {
field_value = NULL;
sqlsrv_free(field_value_temp);
return;
}
if (r == SQL_SUCCESS_WITH_INFO) {
SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = { L'\0' };
SQLSMALLINT len = 0;
stmt->current_results->get_diag_field(1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len);
if (is_truncated_warning(state)) {
SQLLEN chunk_field_len = 0;
// for XML (and possibly other conditions) the field length returned is not the real field length, so
// in every pass, we double the allocation size to retrieve all the contents.
if (field_len_temp == SQL_NO_TOTAL) {
// reset the field_len_temp
field_len_temp = initial_field_len;
do {
SQLLEN buffer_len = field_len_temp;
// Double the size.
field_len_temp *= 2;
field_value_temp = static_cast<char*>(sqlsrv_realloc(field_value_temp, field_len_temp + extra + 1));
field_len_temp -= buffer_len;
// Get the rest of the data
r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp + buffer_len,
field_len_temp + extra, &chunk_field_len, false /*handle_warning*/);
// the last packet will contain the actual amount retrieved, not SQL_NO_TOTAL
// so we calculate the actual length of the string with that.
if (chunk_field_len != SQL_NO_TOTAL)
field_len_temp += chunk_field_len;
else
field_len_temp += buffer_len;
if (r == SQL_SUCCESS_WITH_INFO) {
core::SQLGetDiagField(stmt, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len);
}
} while (r == SQL_SUCCESS_WITH_INFO && is_truncated_warning(state));
} // if (field_len_temp == SQL_NO_TOTAL)
else {
// The field length (or its estimate) is returned, thus no need to double the allocation size.
// Allocate field_len_temp (which is the field length retrieved from the first SQLGetData) but with some padding
// because there is a chance that the estimated field_len_temp is not accurate enough
SQLLEN buffer_len = 50;
field_value_temp = static_cast<char*>(sqlsrv_realloc(field_value_temp, field_len_temp + buffer_len + 1));
field_len_temp -= initial_field_len;
// Get the rest of the data
r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp + initial_field_len,
field_len_temp + buffer_len, &chunk_field_len, false /*handle_warning*/);
field_len_temp = initial_field_len + chunk_field_len;
CHECK_SQL_ERROR_OR_WARNING(r, stmt) {
throw core::CoreException();
}
// Reallocate field_value_temp next
field_value_temp = static_cast<char*>(sqlsrv_realloc(field_value_temp, field_len_temp + extra + 1));
}
} // if (is_truncated_warning(state))
} // if (r == SQL_SUCCESS_WITH_INFO)
CHECK_SQL_ERROR_OR_WARNING(r, stmt) {
throw core::CoreException();
}
if (c_type == SQL_C_WCHAR) {
bool converted = convert_string_from_utf16_inplace(static_cast<SQLSRV_ENCODING>(sqlsrv_php_type.typeinfo.encoding),
&field_value_temp, field_len_temp);
CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) {
throw core::CoreException();
}
}
if (stmt->format_decimals && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) {
// number of decimal places only affect money / smallmoney fields
SQLSMALLINT decimal_places = (stmt->current_meta_data[field_index]->field_is_money_type) ? stmt->decimal_places : NO_CHANGE_DECIMAL_PLACES;
format_decimal_numbers(decimal_places, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp);
}
// finalized the returned values and set field_len to 0 if field_len_temp is negative (which may happen with unixODBC connection pooling)
field_value = field_value_temp;
*field_len = (field_len_temp > 0) ? field_len_temp : 0;
// prevent a warning in debug mode about strings not being NULL terminated. Even though nulls are not necessary, the PHP
// runtime checks to see if a string is null terminated and issues a warning about it if running in debug mode.
// SQL_C_BINARY fields don't return a NULL terminator, so we allocate an extra byte on each field and use the ternary
// operator to set add 1 to fill the null terminator
// with unixODBC connection pooling sometimes field_len_temp can be SQL_NO_DATA.
// In that cause do not set null terminator and set length to 0.
if ( field_len_temp > 0 )
{
// SQL_C_BINARY fields don't return a NULL terminator, so we allocate an extra byte on each field and add 1 to fill the null terminator
if (field_len_temp > 0) {
field_value_temp[field_len_temp] = '\0';
}
else
{
*field_len = 0;
}
}
catch( core::CoreException& ) {
catch (core::CoreException&) {
field_value = NULL;
*field_len = 0;
sqlsrv_free( field_value_temp );
sqlsrv_free(field_value_temp);
throw;
}
catch ( ... ) {
} catch (...) {
field_value = NULL;
*field_len = 0;
sqlsrv_free( field_value_temp );
sqlsrv_free(field_value_temp);
throw;
}
}
// return the option from the stmt_opts array that matches the key. If no option found,
// NULL is returned.
@ -3362,18 +3284,18 @@ void sqlsrv_param_tvp::process_null_param_value(_Inout_ sqlsrv_stmt* stmt)
if (php_type == IS_NULL) {
// This means that the entire column contains nothing but NULLs
sqlsrv_param::process_null_param(param_ptr_z);
}
}
}
void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt)
{
{
core::SQLBindParameter(stmt, param_pos + 1, direction, c_data_type, sql_data_type, column_size, decimal_digits, buffer, buffer_length, &strlen_or_indptr);
// No need to continue if this is one of the constituent columns of the table-valued parameter
if (sql_data_type != SQL_SS_TABLE) {
return;
}
// No need to continue if this is one of the constituent columns of the table-valued parameter
if (sql_data_type != SQL_SS_TABLE) {
return;
}
if (num_rows == 0) {
// TVP has no data
return;
@ -3381,18 +3303,18 @@ void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt)
// Bind the TVP columns one by one
// Register this object first using SQLSetDescField() for sending TVP data post execution
SQLHDESC desc;
core::SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &desc, 0, 0);
SQLRETURN r = ::SQLSetDescField(desc, param_pos + 1, SQL_DESC_DATA_PTR, reinterpret_cast<SQLPOINTER>(this), 0);
SQLHDESC desc;
core::SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &desc, 0, 0);
SQLRETURN r = ::SQLSetDescField(desc, param_pos + 1, SQL_DESC_DATA_PTR, reinterpret_cast<SQLPOINTER>(this), 0);
CHECK_SQL_ERROR_OR_WARNING(r, stmt) {
throw core::CoreException();
}
// First set focus on this parameter
size_t ordinal = param_pos + 1;
core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast<SQLPOINTER>(ordinal), SQL_IS_INTEGER);
// Bind the TVP columns
// First set focus on this parameter
size_t ordinal = param_pos + 1;
core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast<SQLPOINTER>(ordinal), SQL_IS_INTEGER);
// Bind the TVP columns
SQLSRV_ENCODING stmt_encoding = (stmt->encoding() == SQLSRV_ENCODING_DEFAULT) ? stmt->conn->encoding() : stmt->encoding();
HashTable* rows_ht = Z_ARRVAL_P(param_ptr_z);
zval* row_z = zend_hash_index_find(rows_ht, 0);
@ -3428,17 +3350,17 @@ void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt)
column_param->param_ptr_z = data_z;
num_columns++;
} ZEND_HASH_FOREACH_END();
// Process the columns and bind each of them using the saved data
for (int i = 0; i < num_columns; i++) {
sqlsrv_param* column_param = tvp_columns[i];
// Process the columns and bind each of them using the saved data
for (int i = 0; i < num_columns; i++) {
sqlsrv_param* column_param = tvp_columns[i];
column_param->process_param(stmt, NULL);
column_param->bind_param(stmt);
}
// Reset focus
core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast<SQLPOINTER>(0), SQL_IS_INTEGER);
}
// Reset focus
core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast<SQLPOINTER>(0), SQL_IS_INTEGER);
}
// For each of the constituent columns of the table-valued parameter, check its PHP type
@ -3446,21 +3368,21 @@ void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt)
// member placeholder_z
void sqlsrv_param_tvp::populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_ int ordinal)
{
if (sql_data_type == SQL_SS_TABLE || ordinal >= num_rows) {
return;
}
if (sql_data_type == SQL_SS_TABLE || ordinal >= num_rows) {
return;
}
zval* row_z = NULL;
HashTable* values_ht = NULL;
zval* value_z = NULL;
HashTable* values_ht = NULL;
zval* value_z = NULL;
int type = IS_NULL;
switch (param_php_type) {
case IS_TRUE:
case IS_FALSE:
case IS_LONG:
case IS_DOUBLE:
// Find the row from the TVP data based on ordinal
// Find the row from the TVP data based on ordinal
row_z = zend_hash_index_find(Z_ARRVAL_P(parent_tvp->param_ptr_z), ordinal);
if (Z_ISREF_P(row_z)) {
ZVAL_DEREF(row_z);
@ -3507,9 +3429,9 @@ void sqlsrv_param_tvp::populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_
// and param_pos)
bool sqlsrv_param_tvp::send_data_packet(_Inout_ sqlsrv_stmt* stmt)
{
if (sql_data_type != SQL_SS_TABLE) {
if (sql_data_type != SQL_SS_TABLE) {
// This is one of the constituent columns of the table-valued parameter
// Check current_row first
// Check current_row first
if (current_row >= num_rows) {
return false;
}
@ -3590,25 +3512,25 @@ bool sqlsrv_param_tvp::send_data_packet(_Inout_ sqlsrv_stmt* stmt)
break;
}
} // else not IS_NULL
} else {
} else {
// This is the table-valued parameter
if (current_row < num_rows) {
// Loop through the table parameter columns and populate each cell's placeholder whenever applicable
for (size_t i = 0; i < tvp_columns.size(); i++) {
tvp_columns[i]->populate_cell_placeholder(stmt, current_row);
}
// This indicates a TVP row is available
core::SQLPutData(stmt, reinterpret_cast<SQLPOINTER>(1), 1);
current_row++;
} else {
// This indicates there is no more TVP row
core::SQLPutData(stmt, reinterpret_cast<SQLPOINTER>(0), 0);
}
}
// Return false to indicate that the current row has been sent
return false;
if (current_row < num_rows) {
// Loop through the table parameter columns and populate each cell's placeholder whenever applicable
for (size_t i = 0; i < tvp_columns.size(); i++) {
tvp_columns[i]->populate_cell_placeholder(stmt, current_row);
}
// This indicates a TVP row is available
core::SQLPutData(stmt, reinterpret_cast<SQLPOINTER>(1), 1);
current_row++;
} else {
// This indicates there is no more TVP row
core::SQLPutData(stmt, reinterpret_cast<SQLPOINTER>(0), 0);
}
}
// Return false to indicate that the current row has been sent
return false;
}
// A helper method for sending large string data in batches
@ -3618,13 +3540,13 @@ void sqlsrv_param_tvp::send_string_data_in_batches(_Inout_ sqlsrv_stmt* stmt, _I
SQLLEN batch = (encoding == CP_UTF8) ? PHP_STREAM_BUFFER_SIZE / sizeof(SQLWCHAR) : PHP_STREAM_BUFFER_SIZE;
char* p = Z_STRVAL_P(value_z);
while (len > batch) {
core::SQLPutData(stmt, p, batch);
len -= batch;
p += batch;
}
// Put final batch
while (len > batch) {
core::SQLPutData(stmt, p, batch);
len -= batch;
p += batch;
}
// Put final batch
core::SQLPutData(stmt, p, len);
}

View file

@ -0,0 +1,58 @@
--TEST--
Verify Github Issue 1261 is fixed.
--DESCRIPTION--
This test should already pass in Windows so it is mainly aimed for non-Windows settings where UTF-8 is the default encoding. ODBC warnings are handled differently with pdo_sqlsrv so logging is used to checking the warnings.
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
try {
$logFilename = 'php_errors.log';
$logFilepath = dirname(__FILE__).'/'.$logFilename;
ini_set('log_errors', '1');
ini_set('pdo_sqlsrv.log_severity', '2');
ini_set('error_log', $logFilepath);
$conn = new PDO("sqlsrv:Server=$server;Database=$databaseName;", $uid, $pwd);
$conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_SYSTEM);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
# leading string >= 2045 leading to result length > 2048
# column must be VARCHAR(MAX) or VARCHAR(2048) (starts working with bigger VARCHAR(n), e.g. 2060)
# SQLSRV_ATTR_ENCODING must be set to SQLSRV_ENCODING_SYSTEM (works with PDO::SQLSRV_ENCODING_UTF8)
# COLLATE must not be %UTF8% (e.g. Latin1_General_100_CI_AS_SC_UTF8 works)
$sql = "DROP TABLE IF EXISTS #tmpTest;
SET NOCOUNT ON;
DECLARE @val VARCHAR(8000) = REPLICATE('a', 2045) + 'ñ';
CREATE TABLE #tmpTest (testCol VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS);
INSERT INTO #tmpTest (testCol) VALUES (@val);
SELECT * from #tmpTest;";
$stmt = $conn->query($sql);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
var_dump($row);
echo file_get_contents($logFilepath);
unlink($logFilepath);
unset($stmt);
unset($conn);
} catch (PDOException $e) {
var_dump($e);
}
?>
--EXPECTF--
array(1) {
["testCol"]=>
string(2049) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaañ"
}
[%s] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000
[%s] pdo_sqlsrv_db_handle_factory: error code = 5701
[%s] pdo_sqlsrv_db_handle_factory: message = [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Changed database context to '%s'.
[%s] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000
[%s] pdo_sqlsrv_db_handle_factory: error code = 5703
[%s] pdo_sqlsrv_db_handle_factory: message = [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Changed language setting to %s.

View file

@ -0,0 +1,40 @@
--TEST--
Verify Github Issue 1261 is fixed.
--DESCRIPTION--
This test should already pass in Windows so it is mainly aimed for non-Windows settings where UTF-8 is the default encoding.
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
sqlsrv_configure('WarningsReturnAsErrors', 1);
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
require_once('MsCommon.inc');
$conn = connect(array("CharacterSet" => SQLSRV_ENC_CHAR));
# leading string >= 2045 leading to result length > 2048
# column must be VARCHAR(MAX) or VARCHAR(2048) (starts working with bigger VARCHAR(n), e.g. 2060)
# 'CharacterSet' connInfo must be set to SQLSRV_ENC_CHAR (works with UTF-8)
# COLLATE must not be %UTF8% (e.g. Latin1_General_100_CI_AS_SC_UTF8 works)
$sql = "DROP TABLE IF EXISTS #tmpTest;
SET NOCOUNT ON;
DECLARE @val VARCHAR(8000) = REPLICATE('a', 2045) + 'ñ';
CREATE TABLE #tmpTest (testCol VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS);
INSERT INTO #tmpTest (testCol) VALUES (@val);
SELECT * from #tmpTest;";
$stmt = sqlsrv_query($conn, $sql);
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
var_dump($row, $errors);
sqlsrv_free_stmt($stmt);
sqlsrv_close($conn);
?>
--EXPECT--
array(1) {
["testCol"]=>
string(2049) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaañ"
}
NULL