From af61d06bfb05bf44bc4fb69d455df62918ddd291 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Tue, 8 Jun 2021 12:37:28 -0700 Subject: [PATCH] Simplified get_field_as_string and made it more robust (#1265) --- source/shared/core_stmt.cpp | 428 +++++++----------- .../pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt | 58 +++ .../sqlsrv/srv_1261_test_ascii_utf8.phpt | 40 ++ 3 files changed, 273 insertions(+), 253 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt create mode 100644 test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 63920980..968116c3 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -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( 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( 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( 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_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( sqlsrv_malloc( sql_display_size + extra + 1 )); + field_len_temp = initial_field_len; + field_value_temp = static_cast(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(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_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(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(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(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_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(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(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(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(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(0), SQL_IS_INTEGER); + } + + // Reset focus + core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast(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(1), 1); - current_row++; - } else { - // This indicates there is no more TVP row - core::SQLPutData(stmt, reinterpret_cast(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(1), 1); + current_row++; + } else { + // This indicates there is no more TVP row + core::SQLPutData(stmt, reinterpret_cast(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); } diff --git a/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt b/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt new file mode 100644 index 00000000..4e8dcc69 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt @@ -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-- + +--FILE-- +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. + diff --git a/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt b/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt new file mode 100644 index 00000000..bb620e31 --- /dev/null +++ b/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt @@ -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-- + +--FILE-- + 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 \ No newline at end of file