diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index c28aa1bc..448eee04 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -781,8 +781,12 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, "Invalid column number in pdo_sqlsrv_stmt_get_col_data" ); // set the encoding if the user specified one via bindColumn, otherwise use the statement's encoding - sqlsrv_php_type = driver_stmt->sql_type_to_php_type( static_cast( driver_stmt->current_meta_data[colno]->field_type ), - static_cast( driver_stmt->current_meta_data[colno]->field_size ), true ); + // save the php type for next use + sqlsrv_php_type = driver_stmt->sql_type_to_php_type( + static_cast(driver_stmt->current_meta_data[colno]->field_type), + static_cast(driver_stmt->current_meta_data[colno]->field_size), + true); + driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; // if a column is bound to a type different than the column type, figure out a way to convert it to the // type they want @@ -825,6 +829,9 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, break; } } + + // save the php type for the bound column + driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; } SQLSRV_PHPTYPE sqlsrv_phptype_out = SQLSRV_PHPTYPE_INVALID; diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 28f46f68..be6ead07 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1576,15 +1576,23 @@ struct field_meta_data { SQLSMALLINT field_scale; SQLSMALLINT field_is_nullable; bool field_is_money_type; + sqlsrv_phptype sqlsrv_php_type; field_meta_data() : field_name_len(0), field_type(0), field_size(0), field_precision(0), field_scale (0), field_is_nullable(0), field_is_money_type(false) { + reset_php_type(); } ~field_meta_data() { } + + void reset_php_type() + { + sqlsrv_php_type.typeinfo.type = SQLSRV_PHPTYPE_INVALID; + sqlsrv_php_type.typeinfo.encoding = SQLSRV_ENCODING_INVALID; + } }; // *** statement constants *** diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index be1b00ec..090322a5 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -244,6 +244,12 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) // delete sensivity data clean_up_sensitivity_metadata(); + // reset sqlsrv php type in meta data + size_t num_fields = this->current_meta_data.size(); + for (size_t f = 0; f < num_fields; f++) { + this->current_meta_data[f]->reset_php_type(); + } + // create a new result set if( cursor_type == SQLSRV_CURSOR_BUFFERED ) { sqlsrv_malloc_auto_ptr result; @@ -1121,9 +1127,6 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i sqlsrv_phptype sqlsrv_php_type = sqlsrv_php_type_in; - SQLLEN sql_field_type = 0; - SQLLEN sql_field_len = 0; - // Make sure that the statement was executed and not just prepared. CHECK_CUSTOM_ERROR( !stmt->executed, stmt, SQLSRV_ERROR_STATEMENT_NOT_EXECUTED ) { throw core::CoreException(); @@ -1132,37 +1135,47 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i // if the field is to be cached, and this field is being retrieved out of order, cache prior fields so they // may also be retrieved. if( cache_field && (field_index - stmt->last_field_index ) >= 2 ) { - sqlsrv_phptype invalid; - invalid.typeinfo.type = SQLSRV_PHPTYPE_INVALID; - for( int i = stmt->last_field_index + 1; i < field_index; ++i ) { - SQLSRV_ASSERT( reinterpret_cast( zend_hash_index_find_ptr( Z_ARRVAL( stmt->field_cache ), i )) == NULL, "Field already cached." ); - core_sqlsrv_get_field( stmt, i, invalid, prefer_string, field_value, field_len, cache_field, sqlsrv_php_type_out TSRMLS_CC ); - // delete the value returned since we only want it cached, not the actual value - if( field_value ) { - efree( field_value ); - field_value = NULL; - *field_len = 0; - } - } + sqlsrv_phptype invalid; + invalid.typeinfo.type = SQLSRV_PHPTYPE_INVALID; + for( int i = stmt->last_field_index + 1; i < field_index; ++i ) { + SQLSRV_ASSERT( reinterpret_cast( zend_hash_index_find_ptr( Z_ARRVAL( stmt->field_cache ), i )) == NULL, "Field already cached." ); + core_sqlsrv_get_field( stmt, i, invalid, prefer_string, field_value, field_len, cache_field, sqlsrv_php_type_out TSRMLS_CC ); + // delete the value returned since we only want it cached, not the actual value + if( field_value ) { + efree( field_value ); + field_value = NULL; + *field_len = 0; + } + } } // If the php type was not specified set the php type to be the default type. if (sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID) { SQLSRV_ASSERT(stmt->current_meta_data.size() > field_index, "core_sqlsrv_get_field - meta data vector not in sync" ); - sql_field_type = stmt->current_meta_data[field_index]->field_type; - if (stmt->current_meta_data[field_index]->field_precision > 0) { - sql_field_len = stmt->current_meta_data[field_index]->field_precision; + + // Get the corresponding php type from the sql type and then save the result for later + if (stmt->current_meta_data[field_index]->sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID) { + SQLLEN sql_field_type = 0; + SQLLEN sql_field_len = 0; + + sql_field_type = stmt->current_meta_data[field_index]->field_type; + if (stmt->current_meta_data[field_index]->field_precision > 0) { + sql_field_len = stmt->current_meta_data[field_index]->field_precision; + } + else { + sql_field_len = stmt->current_meta_data[field_index]->field_size; + } + sqlsrv_php_type = stmt->sql_type_to_php_type(static_cast(sql_field_type), static_cast(sql_field_len), prefer_string); + stmt->current_meta_data[field_index]->sqlsrv_php_type = sqlsrv_php_type; } else { - sql_field_len = stmt->current_meta_data[field_index]->field_size; + // use the previously saved php type + sqlsrv_php_type = stmt->current_meta_data[field_index]->sqlsrv_php_type; } - - // Get the corresponding php type from the sql type. - sqlsrv_php_type = stmt->sql_type_to_php_type(static_cast(sql_field_type), static_cast(sql_field_len), prefer_string); - } + } // Verify that we have an acceptable type to convert. - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_php_type ), stmt, SQLSRV_ERROR_INVALID_TYPE ) { + CHECK_CUSTOM_ERROR(!is_valid_sqlsrv_phptype(sqlsrv_php_type), stmt, SQLSRV_ERROR_INVALID_TYPE) { throw core::CoreException(); } diff --git a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt new file mode 100644 index 00000000..d41175a2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt @@ -0,0 +1,81 @@ +--TEST-- +GitHub issue #569 - direct query on varchar max fields results in function sequence error (Always Encrypted) +--DESCRIPTION-- +This is similar to pdo_569_query_varcharmax.phpt but is not limited to testing the Always Encrypted feature in Windows only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoTestTable_569_ae'; + createTable($conn, $tableName, array(new ColumnMeta("int", "id", "IDENTITY"), "c1" => "nvarchar(max)")); + + $input = array(); + + $input[0] = 'some very large string'; + $input[1] = '1234567890.1234'; + $input[2] = 'über über'; + + $numRows = 3; + $tsql = "INSERT INTO $tableName (c1) VALUES (?)"; + + $stmt = $conn->prepare($tsql); + for ($i = 0; $i < $numRows; $i++) { + $stmt->bindParam(1, $input[$i]); + $stmt->execute(); + } + + $tsql = "SELECT id, c1 FROM $tableName ORDER BY id"; + $stmt = $conn->prepare($tsql); + $stmt->execute(); + + // Fetch one row each time with different pdo type and/or encoding + $result = $stmt->fetch(PDO::FETCH_NUM); + if ($result[1] !== $input[0]) { + echo "Expected $input[0] but got: "; + var_dump($result[0]); + } + + $stmt->bindColumn(2, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_SYSTEM); + $result = $stmt->fetch(PDO::FETCH_BOUND); + if (!$result || $value !== $input[1]) { + echo "Expected $input[1] but got: "; + var_dump($result); + } + + $stmt->bindColumn(2, $value, PDO::PARAM_STR); + $result = $stmt->fetch(PDO::FETCH_BOUND); + if (!$result || $value !== $input[2]) { + echo "Expected $input[2] but got: "; + var_dump($value); + } + + // Fetch again but all at once + $stmt->execute(); + $rows = $stmt->fetchall(PDO::FETCH_ASSOC); + for ($i = 0; $i < $numRows; $i++) { + $i = $rows[$i]['id'] - 1; + if ($rows[$i]['c1'] !== $input[$i]) { + echo "Expected $input[$i] but got: "; + var_dump($rows[$i]['c1']); + } + } + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + echo $e->getMessage(); +} + +echo "Done\n"; + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt b/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt new file mode 100644 index 00000000..4aca5a59 --- /dev/null +++ b/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt @@ -0,0 +1,96 @@ +--TEST-- +GitHub issue #569 - sqlsrv_query on varchar max fields results in function sequence error +--DESCRIPTION-- +This is similar to srv_569_query_varcharmax.phpt but is not limited to testing the Always Encrypted feature in Windows only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +$tableName = 'srvTestTable_569_ae'; + +$columns = array(new AE\ColumnMeta('int', 'id', 'identity'), + new AE\ColumnMeta('nvarchar(max)', 'c1')); +AE\createTable($conn, $tableName, $columns); + +$input = array(); + +$input[0] = 'some very large string'; +$input[1] = '1234567890.1234'; +$input[2] = 'über über'; + +$numRows = 3; +$isql = "INSERT INTO $tableName (c1) VALUES (?)"; +for ($i = 0; $i < $numRows; $i++) { + $stmt = sqlsrv_prepare($conn, $isql, array($input[$i])); + $result = sqlsrv_execute($stmt); + if (!$result) { + fatalError("Failed to insert row $i into $tableName"); + } +} + +// Select all from test table +$tsql = "SELECT id, c1 FROM $tableName ORDER BY id"; +$stmt = sqlsrv_prepare($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read from $tableName"); +} +$result = sqlsrv_execute($stmt); +if (!$result) { + fatalError("Failed to select data from $tableName"); +} + +// Fetch each row as an array +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + $i = $row['id'] - 1; + if ($row['c1'] !== $input[$i]) { + echo "Expected $input[$i] but got: "; + var_dump($fieldVal); + } +} + +// Fetch again, one field each time +sqlsrv_execute($stmt); + +$i = 0; +while ($i < $numRows) { + sqlsrv_fetch($stmt); + + switch ($i) { + case 0: + $fieldVal = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + break; + case 1: + $stream = sqlsrv_get_field($stmt, 1); + while (!feof( $stream)) { + $fieldVal = fread($stream, 50); + } + break; + default: + $fieldVal = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STRING('utf-8')); + break; + } + + if ($fieldVal !== $input[$i]) { + echo 'Expected $input[$i] but got: '; + var_dump($fieldVal); + } + + $i++; +} + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done \ No newline at end of file