From a6b1cd5d3ad8b301766ec6e96286c25bbd2f3c0a Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Thu, 11 Oct 2018 16:26:20 -0700 Subject: [PATCH 01/10] Added Mojave to macOS instructions (#862) Added Mojave to macOS instructions --- Linux-mac-install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Linux-mac-install.md b/Linux-mac-install.md index 970bc3d0..3988542f 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -9,7 +9,7 @@ These instructions install PHP 7.2 by default -- see the notes at the beginning - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) - [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) - [Installing the drivers on Suse 12](#installing-the-drivers-on-suse-12) -- [Installing the drivers on macOS El Capitan, Sierra and High Sierra](#installing-the-drivers-on-macos-el-capitan-sierra-and-high-sierra) +- [Installing the drivers on macOS El Capitan, Sierra, High Sierra and Mojave](#installing-the-drivers-on-macos-el-capitan-sierra-high-sierra-and-mojave) ## Installing the drivers on Ubuntu 16.04 and 18.04 @@ -209,7 +209,7 @@ sudo systemctl restart apache2 ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on macOS El Capitan, Sierra and High Sierra +## Installing the drivers on macOS El Capitan, Sierra, High Sierra and Mojave If you do not already have it, install brew as follows: ``` From 36fd97e69abdf071cfc6d348c8b225a8f5b984e9 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 12 Oct 2018 14:02:40 -0700 Subject: [PATCH 02/10] Fixed the broken links of Appveyor status badge (#863) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23baf6e8..19a5d88c 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Thank you for taking the time to participate in our last survey. You can continu |--------------------------|--------------------------|---------------------------------------|-------------------------------------------| | [![av-image][]][av-site] | [![tv-image][]][tv-site] | [![Coverage Codecov][]][codecov-site] | [![Coverage Coveralls][]][coveralls-site] | -[av-image]: https://ci.appveyor.com/api/projects/status/xhp4nq9ouljnhxqf/branch/dev?svg=true -[av-site]: https://ci.appveyor.com/project/Microsoft-PHPSQL/msphpsql-frhmr/branch/dev +[av-image]: https://ci.appveyor.com/api/projects/status/vo4rfei6lxlamrnc?svg=true +[av-site]: https://ci.appveyor.com/project/msphpsql/msphpsql/branch/dev [tv-image]: https://travis-ci.org/Microsoft/msphpsql.svg?branch=dev [tv-site]: https://travis-ci.org/Microsoft/msphpsql/ [Coverage Coveralls]: https://coveralls.io/repos/github/Microsoft/msphpsql/badge.svg?branch=dev From 18094a6cefe4536458279fb4d69ac901b047bbc2 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 12 Oct 2018 15:22:27 -0700 Subject: [PATCH 03/10] Feature request 415 for sqlsrv (#861) --- source/shared/core_sqlsrv.h | 95 +++-- source/shared/core_stmt.cpp | 156 +++++++- source/sqlsrv/conn.cpp | 7 + source/sqlsrv/util.cpp | 8 + .../sqlsrv_statement_format_decimals.phpt | 370 ++++++++++++++++++ ...lsrv_statement_format_decimals_scales.phpt | 255 ++++++++++++ 6 files changed, 856 insertions(+), 35 deletions(-) create mode 100644 test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_statement_format_decimals_scales.phpt diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index b623c1b4..35e49cbe 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1107,6 +1107,7 @@ enum SQLSRV_STMT_OPTIONS { SQLSRV_STMT_OPTION_SCROLLABLE, SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE, SQLSRV_STMT_OPTION_DATE_AS_STRING, + SQLSRV_STMT_OPTION_FORMAT_DECIMALS, // Driver specific connection options SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000, @@ -1296,6 +1297,11 @@ struct stmt_option_date_as_string : public stmt_option_functor { virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); }; +struct stmt_option_format_decimals : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); +}; + // used to hold the table for statment options struct stmt_option { @@ -1334,39 +1340,6 @@ extern php_stream_wrapper g_sqlsrv_stream_wrapper; #define SQLSRV_STREAM_WRAPPER "sqlsrv" #define SQLSRV_STREAM "sqlsrv_stream" -// holds the output parameter information. Strings also need the encoding and other information for -// after processing. Only integer, float, and strings are allowable output parameters. -struct sqlsrv_output_param { - - zval* param_z; - SQLSRV_ENCODING encoding; - SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement - SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer - SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary - bool is_bool; - - // string output param constructor - sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) : - param_z(p_z), encoding(enc), param_num(num), original_buffer_len(buffer_len), is_bool(false), php_out_type(SQLSRV_PHPTYPE_INVALID) - { - } - - // every other type output parameter constructor - sqlsrv_output_param( _In_ zval* p_z, _In_ int num, _In_ bool is_bool, _In_ SQLSRV_PHPTYPE php_out_type) : - param_z( p_z ), - encoding( SQLSRV_ENCODING_INVALID ), - param_num( num ), - original_buffer_len( -1 ), - is_bool( is_bool ), - php_out_type(php_out_type) - { - } -}; - -// forward decls -struct sqlsrv_result_set; -struct field_meta_data; - // *** parameter metadata struct *** struct param_meta_data { @@ -1389,6 +1362,59 @@ struct param_meta_data SQLULEN get_column_size() { return column_size; } }; +// holds the output parameter information. Strings also need the encoding and other information for +// after processing. Only integer, float, and strings are allowable output parameters. +struct sqlsrv_output_param { + + zval* param_z; + SQLSRV_ENCODING encoding; + SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement + SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer + SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary + bool is_bool; + param_meta_data meta_data; // parameter meta data + + // string output param constructor + sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) : + param_z(p_z), encoding(enc), param_num(num), original_buffer_len(buffer_len), is_bool(false), php_out_type(SQLSRV_PHPTYPE_INVALID) + { + } + + // every other type output parameter constructor + sqlsrv_output_param( _In_ zval* p_z, _In_ int num, _In_ bool is_bool, _In_ SQLSRV_PHPTYPE php_out_type) : + param_z( p_z ), + encoding( SQLSRV_ENCODING_INVALID ), + param_num( num ), + original_buffer_len( -1 ), + is_bool( is_bool ), + php_out_type(php_out_type) + { + } + + void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE) + { + meta_data.sql_type = sql_type; + meta_data.column_size = column_size; + meta_data.decimal_digits = decimal_digits; + meta_data.nullable = nullable; + } + + SQLSMALLINT getDecimalDigits() + { + // Return decimal_digits only for decimal / numeric types. Otherwise, return -1 + if (meta_data.sql_type == SQL_DECIMAL || meta_data.sql_type == SQL_NUMERIC) { + return meta_data.decimal_digits; + } + else { + return -1; + } + } +}; + +// forward decls +struct sqlsrv_result_set; +struct field_meta_data; + // *** Statement resource structure *** struct sqlsrv_stmt : public sqlsrv_context { @@ -1409,6 +1435,7 @@ struct sqlsrv_stmt : public sqlsrv_context { unsigned long query_timeout; // maximum allowed statement execution time zend_long buffered_query_limit; // maximum allowed memory for a buffered query (measured in KB) bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings + short num_decimals; // indicates number of decimals shown in fetched results (-1 by default, which means no formatting required) // holds output pointers for SQLBindParameter // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving @@ -1743,6 +1770,8 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED, SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, + SQLSRV_ERROR_INVALID_FORMAT_DECIMALS, + SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index ddb1b981..ae86d488 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -107,6 +107,7 @@ void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_ _Out_ SQLSMALLINT& sql_type TSRMLS_DC ); void col_cache_dtor( _Inout_ zval* data_z ); void field_cache_dtor( _Inout_ zval* data_z ); +void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len); void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); 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 TSRMLS_DC ); @@ -141,8 +142,9 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error past_next_result_end( false ), query_timeout( QUERY_TIMEOUT_INVALID ), date_as_string(false), + num_decimals(-1), // -1 means no formatting required buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ), - param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte + param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte send_streams_at_exec( true ), current_stream( NULL, SQLSRV_ENCODING_DEFAULT ), current_stream_read( 0 ) @@ -571,6 +573,8 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_ // save the parameter to be adjusted and/or converted after the results are processed sqlsrv_output_param output_param( param_ref, encoding, param_num, static_cast( buffer_len ) ); + output_param.saveMetaData(sql_type, column_size, decimal_digits); + save_output_param_for_later( stmt, output_param TSRMLS_CC ); // For output parameters, if we set the column_size to be same as the buffer_len, @@ -1416,6 +1420,21 @@ void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op } } +void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC ) +{ + // first check if the input is an integer + CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) { + throw core::CoreException(); + } + + zend_long format_decimals = Z_LVAL_P(value_z); + CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) { + throw core::CoreException(); + } + + stmt->num_decimals = static_cast(format_decimals); +} + // internal function to release the active stream. Called by each main API function // that will alter the statement and cancel any retrieval of data from a stream. void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) @@ -2079,6 +2098,130 @@ void field_cache_dtor( _Inout_ zval* data_z ) sqlsrv_free( cache ); } +// To be called for formatting decimal / numeric fetched values from finalize_output_parameters() and/or get_field_as_string() +void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len) +{ + // In SQL Server, the default maximum precision of numeric and decimal data types is 38 + // + // Note: stmt->num_decimals is -1 by default, which means no formatting on decimals / numerics is necessary + // If the required number of decimals is larger than the field scale, will use the column field scale instead. + // This is to ensure the number of decimals adheres to the column field scale. If smaller, the output value may be rounded up. + // + // Note: it's possible that the decimal / numeric value does not contain a decimal dot because the field scale is 0. + // Thus, first check if the decimal dot exists. If not, no formatting necessary, regardless of decimals_digits + // + std::string str = field_value; + size_t pos = str.find_first_of('.'); + + if (pos == std::string::npos || decimals_digits < 0) { + return; + } + + SQLSMALLINT num_decimals = decimals_digits; + if (num_decimals > field_scale) { + num_decimals = field_scale; + } + + // We want the rounding to be consistent with php number_format(), http://php.net/manual/en/function.number-format.php + // as well as SQL Server Management studio, such that the least significant digit will be rounded up if it is + // followed by 5 or above. + + bool isNegative = false; + + // If negative, remove the minus sign for now so as not to complicate the rounding process + if (str[0] == '-') { + isNegative = true; + std::ostringstream oss; + oss << str.substr(1); + str = oss.str(); + pos = str.find_first_of('.'); + } + + // Adds the leading zero if not exists + if (pos == 0) { + std::ostringstream oss; + oss << '0' << str; + str = oss.str(); + pos++; + } + + size_t last = 0; + if (num_decimals == 0) { + // Chop all decimal digits, including the decimal dot + size_t pos2 = pos + 1; + short n = str[pos2] - '0'; + if (n >= 5) { + // Start rounding up - starting from the digit left of the dot all the way to the first digit + bool carry_over = true; + for (short p = pos - 1; p >= 0 && carry_over; p--) { + n = str[p] - '0'; + if (n == 9) { + str[p] = '0' ; + carry_over = true; + } + else { + n++; + carry_over = false; + str[p] = '0' + n; + } + } + if (carry_over) { + std::ostringstream oss; + oss << '1' << str.substr(0, pos); + str = oss.str(); + pos++; + } + } + last = pos; + } + else { + size_t pos2 = pos + num_decimals + 1; + // No need to check if rounding is necessary when pos2 has passed the last digit in the input string + if (pos2 < str.length()) { + short n = str[pos2] - '0'; + if (n >= 5) { + // Start rounding up - starting from the digit left of pos2 all the way to the first digit + bool carry_over = true; + for (short p = pos2 - 1; p >= 0 && carry_over; p--) { + if (str[p] == '.') { // Skip the dot + continue; + } + n = str[p] - '0'; + if (n == 9) { + str[p] = '0' ; + carry_over = true; + } + else { + n++; + carry_over = false; + str[p] = '0' + n; + } + } + if (carry_over) { + std::ostringstream oss; + oss << '1' << str.substr(0, pos2); + str = oss.str(); + pos2++; + } + } + } + last = pos2; + } + + // Add the minus sign back if negative + if (isNegative) { + std::ostringstream oss; + oss << '-' << str.substr(0, last); + str = oss.str(); + } else { + str = str.substr(0, last); + } + + size_t len = str.length(); + str.copy(field_value, len); + field_value[len] = '\0'; + *field_len = len; +} // To be called after all results are processed. ODBC and SQL Server do not guarantee that all output // parameters will be present until all results are processed (since output parameters can depend on results @@ -2160,6 +2303,11 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) core::sqlsrv_zval_stringl(value_z, str, str_len); } else { + SQLSMALLINT decimal_digits = output_param->getDecimalDigits(); + if (stmt->num_decimals >= 0 && decimal_digits >= 0) { + format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len); + } + core::sqlsrv_zval_stringl(value_z, str, str_len); } } @@ -2214,7 +2362,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind { SQLRETURN r; SQLSMALLINT c_type; - SQLLEN sql_field_type = 0; + SQLSMALLINT sql_field_type = 0; SQLSMALLINT extra = 0; SQLLEN field_len_temp = 0; SQLLEN sql_display_size = 0; @@ -2425,6 +2573,10 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind throw core::CoreException(); } } + + if (stmt->num_decimals >= 0 && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) { + format_decimal_numbers(stmt->num_decimals, 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 { diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index dcfc755e..d5f324a2 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -174,6 +174,7 @@ namespace SSStmtOptionNames { const char SCROLLABLE[] = "Scrollable"; const char CLIENT_BUFFER_MAX_SIZE[] = INI_BUFFERED_QUERY_LIMIT; const char DATE_AS_STRING[] = "ReturnDatesAsStrings"; + const char FORMAT_DECIMALS[] = "FormatDecimals"; } namespace SSConnOptionNames { @@ -250,6 +251,12 @@ const stmt_option SS_STMT_OPTS[] = { SQLSRV_STMT_OPTION_DATE_AS_STRING, std::unique_ptr( new stmt_option_date_as_string ) }, + { + SSStmtOptionNames::FORMAT_DECIMALS, + sizeof( SSStmtOptionNames::FORMAT_DECIMALS ), + SQLSRV_STMT_OPTION_FORMAT_DECIMALS, + std::unique_ptr( new stmt_option_format_decimals ) + }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index aab5e2eb..545f699d 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -428,6 +428,14 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, { IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false} }, + { + SQLSRV_ERROR_INVALID_FORMAT_DECIMALS, + { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -117, false} + }, + { + SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, + { IMSSP, (SQLCHAR*) "For formatting decimal data values, %1!d! is out of range. Expected an integer from 0 to 38, inclusive.", -118, true} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt new file mode 100644 index 00000000..7e9e4b24 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt @@ -0,0 +1,370 @@ +--TEST-- +Test how decimal data output values can be formatted (feature request issue 415) +--DESCRIPTION-- +Test how numeric and decimal data output values can be formatted by using the +statement option FormatDecimals, which expects an integer value from the range [0,38], +affecting only the money / decimal types in the fetched result set because they are +always strings to preserve accuracy and precision, unlike other primitive numeric +types that can be retrieved as numbers. + +No effect on other operations like insertion or update. + +1. By default, data will be returned with the original precision and scale +2. The data column original scale still takes precedence – for example, if the user +specifies 3 decimal digits for a column of decimal(5,2), the result still shows only 2 +decimals to the right of the dot +3. After formatting, the missing leading zeroes will be padded +4. The underlying data will not be altered, but formatted results may likely be rounded +up (e.g. .2954 will be displayed as 0.30 if the user wants only two decimals) +5. For output params use SQLSRV_SQLTYPE_DECIMAL with the correct precision and scale +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $fieldScale, will show $fieldScale decimal digits + if ($formatDecimal >= 0) { + $numDecimals = ($formatDecimal > $fieldScale) ? $fieldScale : $formatDecimal; + } else { + $numDecimals = $fieldScale; + } + $expected = number_format($input, $numDecimals); + if ($actual === $expected) { + $matched = true; + } else { + echo "For $column: expected $expected but the value is $actual\n"; + } + } + return $matched; +} + +function testErrorCases($conn) +{ + $query = "SELECT 0.0001"; + + $options = array('FormatDecimals' => 1.5); + $stmt = sqlsrv_query($conn, $query, array(), $options); + if ($stmt) { + fatalError("Case 1: expected query to fail!!"); + } else { + $error = sqlsrv_errors()[0]['message']; + $message = 'Expected an integer to specify number of decimals to format the output values of decimal data types.'; + + if (strpos($error, $message) === false) { + print_r(sqlsrv_errors()); + } + } + + $options = array('FormatDecimals' => -1); + $stmt = sqlsrv_query($conn, $query, array(), $options); + if ($stmt) { + fatalError("Case 2: expected query to fail!!"); + } else { + $error = sqlsrv_errors()[0]['message']; + $message = 'For formatting decimal data values, -1 is out of range. Expected an integer from 0 to 38, inclusive.'; + + if (strpos($error, $message) === false) { + print_r(sqlsrv_errors()); + } + } +} + +function testFloatTypes($conn) +{ + // This test with the float types of various number of bits, which are retrieved + // as numbers by default. When fetched as strings, no formatting is done even with + // the statement option FormatDecimals set + $values = array('2.9978', '-0.2982', '33.2434', '329.690734', '110.913498'); + $epsilon = 0.001; + + $query = "SELECT CONVERT(float(1), $values[0]), + CONVERT(float(12), $values[1]), + CONVERT(float(24), $values[2]), + CONVERT(float(36), $values[3]), + CONVERT(float(53), $values[4])"; + + $stmt = sqlsrv_query($conn, $query); + $floats = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + if (!$floats) { + echo "testFloatTypes: sqlsrv_fetch_array failed\n"; + } + + // Set FormatDecimals to 2, but the number of decimals in each of the results + // will vary -- FormatDecimals has no effect + $numDigits = 2; + $options = array('FormatDecimals' => $numDigits); + $stmt = sqlsrv_query($conn, $query, array(), $options); + if (sqlsrv_fetch($stmt)) { + for ($i = 0; $i < count($values); $i++) { + $floatStr = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + $numbers = explode('.', $floatStr); + $len = strlen($numbers[1]); + if ($len == $numDigits) { + // This is highly unlikely + var_dump($floatStr); + } + $floatVal = floatval($floatStr); + $diff = abs($floatVal - $floats[$i]) / $floats[$i]; + if ($diff > $epsilon) { + var_dump($diff); + var_dump($floatVal); + } + } + } else { + echo "testFloatTypes: sqlsrv_fetch failed\n"; + } +} + +function testMoneyTypes($conn) +{ + // With money and smallmoney types, which are essentially decimal types + // ODBC driver does not support Always Encrypted feature with money / smallmoney + $values = array('1.9954', '0', '-0.5', '0.2954', '9.6789', '99.991'); + $defaults = array('1.9954', '.0000', '-.5000', '.2954', '9.6789', '99.9910'); + + $query = "SELECT CONVERT(smallmoney, $values[0]), + CONVERT(money, $values[1]), + CONVERT(smallmoney, $values[2]), + CONVERT(money, $values[3]), + CONVERT(smallmoney, $values[4]), + CONVERT(money, $values[5])"; + + $stmt = sqlsrv_query($conn, $query); + $results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + for ($i = 0; $i < count($values); $i++) { + if ($defaults[$i] !== $results[$i]) { + echo "testMoneyTypes: Expected default $defaults[$i] but got $results[$i]\n"; + } + } + + // Set FormatDecimals to 0 decimal digits + $numDigits = 0; + $options = array('FormatDecimals' => $numDigits); + $stmt = sqlsrv_query($conn, $query, array(), $options); + $results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + for ($i = 0; $i < count($values); $i++) { + $value = number_format($values[$i], $numDigits); + if ($value !== $results[$i]) { + echo "testMoneyTypes: Expected $value but got $results[$i]\n"; + } + } + + // Set FormatDecimals to 2 decimal digits + $numDigits = 2; + $options = array('FormatDecimals' => $numDigits); + $stmt = sqlsrv_query($conn, $query, array(), $options); + $results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + for ($i = 0; $i < count($values); $i++) { + $value = number_format($values[$i], $numDigits); + if ($value !== $results[$i]) { + echo "testMoneyTypes: Expected $value but got $results[$i]\n"; + } + } +} + +function testNoOption($conn, $tableName, $inputs, $columns, $exec) +{ + // Without the statement option, should return decimal values as they are + $query = "SELECT * FROM $tableName"; + if ($exec) { + $stmt = sqlsrv_query($conn, $query); + } else { + $stmt = sqlsrv_prepare($conn, $query); + sqlsrv_execute($stmt); + } + + // Compare values + $results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + for ($i = 0; $i < count($inputs); $i++) { + compareNumbers($results[$i], $inputs[$i], $columns[$i], $i); + } +} + +function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $withBuffer) +{ + // Decimal values should return decimal digits based on the valid statement + // option FormatDecimals + $query = "SELECT * FROM $tableName"; + if ($withBuffer){ + $options = array('Scrollable' => 'buffered', 'FormatDecimals' => $formatDecimal); + } else { + $options = array('FormatDecimals' => $formatDecimal); + } + + $size = count($inputs); + $stmt = sqlsrv_prepare($conn, $query, array(), $options); + + // Fetch by getting one field at a time + sqlsrv_execute($stmt); + + if (sqlsrv_fetch($stmt) === false) { + fatalError("Failed in retrieving data\n"); + } + for ($i = 0; $i < $size; $i++) { + $field = sqlsrv_get_field($stmt, $i); // Expect a string + compareNumbers($field, $inputs[$i], $columns[$i], $i, $formatDecimal); + } +} + +function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale) +{ + $outString = ''; + $numDigits = 2; + + // Derive the sqlsrv type SQLSRV_SQLTYPE_DECIMAL($prec, $scale) + $sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale); + + $outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1); + $stmt = sqlsrv_prepare($conn, $outSql, + array(array(&$outString, SQLSRV_PARAM_OUT, null, $sqlType)), + array('FormatDecimals' => $numDigits)); + if (!$stmt) { + fatalError("getOutputParam: failed when preparing to call $storedProcName"); + } + if (!sqlsrv_execute($stmt)) { + fatalError("getOutputParam: failed to execute procedure $storedProcName"); + } + + // The output param should have been formatted based on $numDigits, if less + // than $scale + $column = 'outputParam'; + compareNumbers($outString, $inputValue, $column, $scale, $numDigits); + sqlsrv_free_stmt($stmt); + + if (!AE\isColEncrypted()) { + // Get output param without specifying sqlsrv type, and the returned value will + // be a regular string -- its value should be the same as the input value, + // unaffected by the statement option FormatDecimals + // With ColumnEncryption enabled, the driver is able to derive the decimal type, + // so skip this part of the test + $outString2 = ''; + $stmt = sqlsrv_prepare($conn, $outSql, + array(array(&$outString2, SQLSRV_PARAM_OUT)), + array('FormatDecimals' => $numDigits)); + if (!$stmt) { + fatalError("getOutputParam2: failed when preparing to call $storedProcName"); + } + if (!sqlsrv_execute($stmt)) { + fatalError("getOutputParam2: failed to execute procedure $storedProcName"); + } + + $column = 'outputParam2'; + compareNumbers($outString2, $inputValue, $column, $scale); + sqlsrv_free_stmt($stmt); + } +} + +function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes) +{ + for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) { + // Create the stored procedure first + $storedProcName = "spFormatDecimals" . $i; + $procArgs = "@col $dataTypes[$i] OUTPUT"; + $procCode = "SELECT @col = $columns[$i] FROM $tableName"; + createProc($conn, $storedProcName, $procArgs, $procCode); + + // Call stored procedure to retrieve output param + getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i); + + dropProc($conn, $storedProcName); + } +} + +set_time_limit(0); +sqlsrv_configure('WarningsReturnAsErrors', 1); + +$conn = AE\connect(); +if (!$conn) { + fatalError("Could not connect.\n"); +} + +// First to test if leading zero is added +testMoneyTypes($conn); + +// Then test error conditions +testErrorCases($conn); + +// Also test using regular floats +testFloatTypes($conn); + +// Create the test table of decimal / numeric data columns +$tableName = 'sqlsrvFormatDecimals'; + +$columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6'); +$dataTypes = array('decimal(3,0)', 'decimal(4,1)', 'decimal(5,2)', 'numeric(6,3)', 'numeric(7,4)', 'numeric(8, 5)'); + +$colMeta = array(new AE\ColumnMeta($dataTypes[0], $columns[0]), + new AE\ColumnMeta($dataTypes[1], $columns[1]), + new AE\ColumnMeta($dataTypes[2], $columns[2]), + new AE\ColumnMeta($dataTypes[3], $columns[3]), + new AE\ColumnMeta($dataTypes[4], $columns[4]), + new AE\ColumnMeta($dataTypes[5], $columns[5])); +AE\createTable($conn, $tableName, $colMeta); + +// Generate random input values based on precision and scale +$values = array(); +$max2 = 1; +for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) { + // First get a random number + $n = rand(0, 10); + $neg = ($n % 2 == 0) ? -1 : 1; + + // $n1 may or may not be negative + $max1 = 1000; + $n1 = rand(0, $max1) * $neg; + + if ($s > 0) { + $max2 *= 10; + $n2 = rand(0, $max2); + $number = sprintf("%d.%d", $n1, $n2); + } else { + $number = sprintf("%d", $n1); + } + + array_push($values, $number); +} + +// Insert data values as strings +$inputData = array($colMeta[0]->colName => $values[0], + $colMeta[1]->colName => $values[1], + $colMeta[2]->colName => $values[2], + $colMeta[3]->colName => $values[3], + $colMeta[4]->colName => $values[4], + $colMeta[5]->colName => $values[5]); +$stmt = AE\insertRow($conn, $tableName, $inputData); +if (!$stmt) { + var_dump($values); + fatalError("Failed to insert data.\n"); +} +sqlsrv_free_stmt($stmt); + +testNoOption($conn, $tableName, $values, $columns, true); +testNoOption($conn, $tableName, $values, $columns, false); + +// Now try with setting number decimals to 3 then 2 +testStmtOption($conn, $tableName, $values, $columns, 3, false); +testStmtOption($conn, $tableName, $values, $columns, 3, true); + +testStmtOption($conn, $tableName, $values, $columns, 2, false); +testStmtOption($conn, $tableName, $values, $columns, 2, true); + +// Test output parameters +testOutputParam($conn, $tableName, $values, $columns, $dataTypes); + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_decimals_scales.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_decimals_scales.phpt new file mode 100644 index 00000000..4abb0398 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_statement_format_decimals_scales.phpt @@ -0,0 +1,255 @@ +--TEST-- +Test various precisions of formatting decimal data output values (feature request issue 415) +--DESCRIPTION-- +In SQL Server, the maximum allowed precision is 38. The scale can range from 0 up to the +defined precision. Generate a long numeric string and get rid of the last digit to make it a +39-digit-string. Then replace one digit at a time with a dot '.' to make it a decimal +input string for testing with various scales. +For example, +string(39) ".23456789012345678901234567890123456789" +string(39) "1.3456789012345678901234567890123456789" +string(39) "12.456789012345678901234567890123456789" +string(39) "123.56789012345678901234567890123456789" +string(39) "1234.6789012345678901234567890123456789" +string(39) "12345.789012345678901234567890123456789" +... ... +string(39) "1234567890123456789012345678901234.6789" +string(39) "12345678901234567890123456789012345.789" +string(39) "123456789012345678901234567890123456.89" +string(39) "1234567890123456789012345678901234567.9" +string(38) "12345678901234567890123456789012345678" + +Note: PHP number_format() will not be used for verification in this test +because the function starts losing accuracy with large number of precisions / scales. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $digits)); + + // Restore the $i-th digit with its original digit + $digits[$i] = $d; + } + + $stmt = AE\insertRow($conn, $tableName, $inputData); + if (!$stmt) { + fatalError("Failed to insert data\n"); + } + sqlsrv_free_stmt($stmt); + + return $inputData; +} + +function verifyNoDecimals($value, $input, $round) +{ + global $prec, $dot; + + // Use PHP explode() to separate the input string into an array + $parts = explode($dot, $input); + $len = strlen($parts[0]); + if ($len == 0) { + // The original input string is missing a leading zero + $parts[0] = '0'; + } + + // No need to worry about carry over for the input data of this test + // Check the first digit of $parts[1] + if ($len < $prec) { + // Only need to round up when $len < $prec + $ch = $parts[1][0]; + + // Round the last digit of $parts[0] if $ch is '5' or above + if ($ch >= '5') { + $len = strlen($parts[0]); + $parts[0][$len-1] = $parts[0][$len-1] + 1 + '0'; + } + } + + // No decimal digits left in the expected string + $expected = $parts[0]; + if ($value !== $expected) { + echo "Round $round scale 0: expected $expected but returned $value\n"; + } +} + +function verifyWithDecimals($value, $input, $round, $scale) +{ + global $dot; + + // Use PHP explode() to separate the input string into an array + $parts = explode($dot, $input); + if (strlen($parts[0]) == 0) { + // The original input string is missing a leading zero + $parts[0] = '0'; + } + + // No need to worry about carry over for the input data of this test + // Check the digit at the position $scale of $parts[1] + $len = strlen($parts[1]); + if ($scale < $len) { + // Only need to round up when $scale < $len + $ch = $parts[1][$scale]; + + // Round the previous digit if $ch is '5' or above + if ($ch >= '5') { + $parts[1][$scale-1] = $parts[1][$scale-1] + 1 + '0'; + } + } + + // Use substr() to get up to $scale + $parts[1] = substr($parts[1], 0, $scale); + + // Join the array elements together + $expected = implode($dot, $parts); + if ($value !== $expected) { + echo "Round $round scale $scale: expected $expected but returned $value\n"; + } +} + +/**** +The function testVariousScales() will fetch one column at a time, using scale from +0 up to the maximum scale allowed for that column type. + +For example, for column of type decimal(38,4), the input string is +1234567890123456789012345678901234.6789 + +When fetching data, using scale from 0 to 4, the following values are expected to return: +1234567890123456789012345678901235 +1234567890123456789012345678901234.7 +1234567890123456789012345678901234.68 +1234567890123456789012345678901234.679 +1234567890123456789012345678901234.6789 + +For example, for column of type decimal(38,6), the input string is +12345678901234567890123456789012.456789 + +When fetching data, using scale from 0 to 6, the following values are expected to return: +12345678901234567890123456789012 +12345678901234567890123456789012.5 +12345678901234567890123456789012.46 +12345678901234567890123456789012.457 +12345678901234567890123456789012.4568 +12345678901234567890123456789012.45679 +12345678901234567890123456789012.456789 + +etc. +****/ +function testVariousScales($conn, $tableName, $inputData) +{ + global $prec; + $max = $prec + 1; + + for ($i = 0; $i < $max; $i++) { + $scale = $prec - $i; + $column = "col_$scale"; + + $query = "SELECT $column as col1 FROM $tableName"; + $input = $inputData[$column]; + + // Default case: the fetched value should be the same as the corresponding input + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + fatalError("In testVariousScales: failed in default case\n"); + } + if ($obj = sqlsrv_fetch_object($stmt)) { + trace("\n$obj->col1\n"); + if ($obj->col1 !== $input) { + echo "default case: expected $input but returned $obj->col1\n"; + } + } else { + fatalError("In testVariousScales: sqlsrv_fetch_object failed\n"); + } + + // Next, format how many decimal digits to be displayed + $query = "SELECT $column FROM $tableName"; + for ($j = 0; $j <= $scale; $j++) { + $options = array('FormatDecimals' => $j); + $stmt = sqlsrv_query($conn, $query, array(), $options); + + if (sqlsrv_fetch($stmt)) { + $value = sqlsrv_get_field($stmt, 0); + trace("$value\n"); + + if ($j == 0) { + verifyNoDecimals($value, $input, $i); + } else { + verifyWithDecimals($value, $input, $i, $j); + } + } else { + fatalError("Round $i scale $j: sqlsrv_fetch failed\n"); + } + } + } +} + +set_time_limit(0); +sqlsrv_configure('WarningsReturnAsErrors', 1); + +$conn = AE\connect(); +if (!$conn) { + fatalError("Could not connect.\n"); +} + +$tableName = createTestTable($conn); +$inputData = insertTestData($conn, $tableName); +testVariousScales($conn, $tableName, $inputData); + +dropTable($conn, $tableName); + +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file From b3072a99eeb33a445f815d99230d3437e1574bbb Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 19 Oct 2018 14:48:21 -0700 Subject: [PATCH 04/10] Modified how to send stream data using SQLPutData and SQLParamData (#865) --- Dockerfile-msphpsql | 6 +- source/shared/core_sqlsrv.h | 3 +- source/shared/core_stmt.cpp | 23 ++-- .../pdo_sqlsrv/MsData_PDO_AllTypes.inc | 2 +- .../pdo_sqlsrv/pdostatement_GetDataType.phpt | Bin 5677 -> 5692 bytes .../pdostatement_bindParam_empty_binary.phpt | 92 +++++++++++++++ .../pdo_sqlsrv/pdostatement_fetchAll.phpt | Bin 17989 -> 18019 bytes .../pdo_sqlsrv/pdostatement_fetchObject.phpt | Bin 3978 -> 3993 bytes .../pdo_sqlsrv/pdostatement_nextRowset.phpt | Bin 10727 -> 10757 bytes .../sqlsrv/sqlsrv_param_empty_binary.phpt | 105 ++++++++++++++++++ test/functional/sqlsrv/test_largeData.phpt | 6 + 11 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdostatement_bindParam_empty_binary.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_param_empty_binary.phpt diff --git a/Dockerfile-msphpsql b/Dockerfile-msphpsql index 43435eb3..ab289479 100644 --- a/Dockerfile-msphpsql +++ b/Dockerfile-msphpsql @@ -44,8 +44,10 @@ RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools ENV PATH="/opt/mssql-tools/bin:${PATH}" -#install coveralls -RUN python -m pip install --upgrade pip && pip install cpp-coveralls +#install coveralls (upgrade both pip and requests first) +RUN python -m pip install --upgrade pip +RUN python -m pip install --upgrade requests +RUN python -m pip install cpp-coveralls #Either Install git / download zip (One can see other strategies : https://ryanfb.github.io/etc/2015/07/29/git_strategies_for_docker.html ) #One option is to get source from zip file of repository. diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 35e49cbe..59b5afcd 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1939,9 +1939,10 @@ namespace core { inline void check_for_mars_error( _Inout_ sqlsrv_stmt* stmt, _In_ SQLRETURN r TSRMLS_DC ) { + // Skip this if not SQL_ERROR - // We check for the 'connection busy' error caused by having MultipleActiveResultSets off // and return a more helpful message prepended to the ODBC errors if that error occurs - if( !SQL_SUCCEEDED( r )) { + if (r == SQL_ERROR) { SQLCHAR err_msg[SQL_MAX_MESSAGE_LENGTH + 1] = {'\0'}; SQLSMALLINT len = 0; diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index ae86d488..58ec4520 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -1298,15 +1298,15 @@ bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) php_stream* param_stream = NULL; core::sqlsrv_php_stream_from_zval_no_verify( *stmt, param_stream, stmt->current_stream.stream_z TSRMLS_CC ); - // if we're at the end, then release our current parameter - if( php_stream_eof( param_stream )) { - // if no data was actually sent prior, then send a NULL - if( stmt->current_stream_read == 0 ) { - // send an empty string, which is what a 0 length does. - char buff[1]; // temp storage to hand to SQLPutData - core::SQLPutData( stmt, buff, 0 TSRMLS_CC ); + // if we're at the end, then reset both current_stream and current_stream_read + if (php_stream_eof(param_stream)) { + // yet return to the very beginning of param_stream since SQLParamData() may ask for the same data again + int ret = php_stream_seek(param_stream, 0, SEEK_SET); + if (ret != 0) { + LOG(SEV_ERROR, "PHP stream: stream seek failed."); + throw core::CoreException(); } - stmt->current_stream = sqlsrv_stream( NULL, SQLSRV_ENCODING_CHAR ); + stmt->current_stream = sqlsrv_stream(NULL, SQLSRV_ENCODING_CHAR); stmt->current_stream_read = 0; } // read the data from the stream, send it via SQLPutData and track how much we've sent. @@ -1322,7 +1322,12 @@ bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) } stmt->current_stream_read += static_cast( read ); - if( read > 0 ) { + if (read == 0) { + // send an empty string, which is what a 0 length does. + char buff[1]; // temp storage to hand to SQLPutData + core::SQLPutData(stmt, buff, 0 TSRMLS_CC); + } + else if (read > 0) { // if this is a UTF-8 stream, then we will use the UTF-8 encoding to determine if we're in the middle of a character // then read in the appropriate number more bytes and then retest the string. This way we try at most to convert it // twice. diff --git a/test/functional/pdo_sqlsrv/MsData_PDO_AllTypes.inc b/test/functional/pdo_sqlsrv/MsData_PDO_AllTypes.inc index 14f89a1a..2fe6d98a 100644 --- a/test/functional/pdo_sqlsrv/MsData_PDO_AllTypes.inc +++ b/test/functional/pdo_sqlsrv/MsData_PDO_AllTypes.inc @@ -12,7 +12,7 @@ $int_col = array(1, 2); $bin = fopen('php://memory', 'a'); -fwrite($bin, '00'); +fwrite($bin, hex2bin('6162636465')); // 'abcde' rewind($bin); $binary_col = array($bin, $bin); diff --git a/test/functional/pdo_sqlsrv/pdostatement_GetDataType.phpt b/test/functional/pdo_sqlsrv/pdostatement_GetDataType.phpt index 99c9b6c5dc9850dcfa24e9e9a65410ba1ee69fb8..bdd87ffe06ab3d07ebd19f8aba3d6bfc6f9d8503 100644 GIT binary patch delta 61 vcmZ3hvqxvcPcBYVO$DXIq~w&;$$z +--FILE-- +prepare($query); + $stmt->bindParam(1, $bin, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $bin, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(3, $bin, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + + $stmt->execute(); + fclose($bin); + + $bin2 = fopen('php://memory', 'a'); + fwrite($bin2, $inputs[1]); // 'ABC' will be 0x414243 in hex + rewind($bin2); + + $stmt->bindParam(1, $bin2, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $bin2, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(3, $bin2, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + + $stmt->execute(); + fclose($bin2); + + // Verify the data by fetching and comparing against the inputs + $query = "SELECT * FROM $tableName"; + $stmt = $conn->query($query); + $rowset = $stmt->fetchAll(); + + for ($i = 0; $i < 2; $i++) { + for ($j = 0; $j < 3; $j++) { + $str = $rowset[$i][$j]; + $len = strlen($str); + $failed = false; + + if ($j == 0) { + // binary fields have fixed size, unlike varbinary ones + if ($len !== $size || trim($str) !== $inputs[$i]) { + $failed = true; + } + } else { + if ($len !== strlen($inputs[$i]) || $str !== $inputs[$i]) { + $failed = true; + } + } + + if ($failed) { + $row = $i + 1; + $col = $j + 1; + echo "Unexpected value returned from row $row and column $col: \n"; + var_dump($str); + } + } + } + + dropTable($conn, $tableName); + unset($stmt); + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); + exit; +} +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt index dc7ccae64c4d9fda3c8e9e985b30d2227ec19ae5..62bc542d8820bf95e3b41f4271db38eefba6a94f 100644 GIT binary patch delta 139 zcmX@w!}z#|al;Nt*2JXbl+?-ldBizQH5HV=yv=td1(*@s$sc7T5WLO2vZwhF+{t&e okt8So&{o%io1?^~00hxS=CQVRU}kYiQD$Dc23)~rdmT0@00+P+bpQYW delta 128 zcmaFd!+5lZal;NtRs#bD28PM|dBizQH5HV=yv=td1(+2KfLtXm1t5qvG>Eme12chnY1oDLEx|@W#uh4dyCBx1^_>EEg1j+ delta 112 zcmZn-c^hndyDfPsNw@ +--FILE-- + $inputs[0], "VarBinaryCol" => $inputs[1], "VarBinaryMaxCol" => $inputs[2]), $r, AE\INSERT_PREPARE_PARAMS); + + +$inputs = array(new AE\BindParamOption($inputValues[1], + null, + "SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY)", + "SQLSRV_SQLTYPE_BINARY($size)"), + new AE\BindParamOption($inputValues[1], + null, + "SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY)", + "SQLSRV_SQLTYPE_VARBINARY($size)"), + new AE\BindParamOption($inputValues[1], + null, + "SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY)", + "SQLSRV_SQLTYPE_VARBINARY('max')")); +$r; +$stmt = AE\insertRow($conn, $tableName, array("BinaryCol" => $inputs[0], "VarBinaryCol" => $inputs[1], "VarBinaryMaxCol" => $inputs[2]), $r, AE\INSERT_PREPARE_PARAMS); + +// Verify the data by fetching and comparing against the inputs +$query = "SELECT * FROM $tableName"; +$stmt = sqlsrv_query($conn, $query); +if (!$stmt) { + fatalError("Failed to retrieve data from $tableName"); +} + +for ($i = 0; $i < 2; $i++) { + $rowNum = $i + 1; + $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); + if (!$row) { + fatalError("Failed in sqlsrv_fetch_array for row $rowNum"); + } + + for ($j = 0; $j < 3; $j++) { + $str = $row[$j]; + $len = strlen($str); + $failed = false; + + if ($j == 0) { + // binary fields have fixed size, unlike varbinary ones + if ($len !== $size || trim($str) !== $inputValues[$i]) { + $failed = true; + } + } else { + $inputLen = strlen($inputValues[$i]); + if ($len !== $inputLen || $str !== $inputValues[$i]) { + $failed = true; + } + } + + if ($failed) { + $colNum = $j + 1; + echo "Unexpected value returned from row $rowNum and column $colNum: \n"; + var_dump($str); + } + } +} + +dropTable($conn, $tableName); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +echo "Done\n"; + +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/test_largeData.phpt b/test/functional/sqlsrv/test_largeData.phpt index 5f1969ae..e282df2a 100644 --- a/test/functional/sqlsrv/test_largeData.phpt +++ b/test/functional/sqlsrv/test_largeData.phpt @@ -42,6 +42,12 @@ class my_stream { function stream_seek($offset, $whence) { + // For the purpose of this test only support SEEK_SET to $offset 0 + if ($whence == SEEK_SET && $offset == 0) { + $this->total_read = $offset; + return true; + } + return false; } } From 2a9398f7e0aaf29f30d6b35cf0a673b61f81a98c Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Wed, 24 Oct 2018 12:37:14 -0700 Subject: [PATCH 05/10] Updated instructions to include Ubuntu 18.10 (#869) --- Linux-mac-install.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Linux-mac-install.md b/Linux-mac-install.md index 3988542f..4d4eb3ce 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -5,30 +5,33 @@ These instructions install PHP 7.2 by default -- see the notes at the beginning ## Contents of this page: -- [Installing the drivers on Ubuntu 16.04 and 18.04](#installing-the-drivers-on-ubuntu-1604-and-1804) +- [Installing the drivers on Ubuntu 16.04, 18.04 and 18.10](#installing-the-drivers-on-ubuntu-1604-1804-and-1810) - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) - [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) - [Installing the drivers on Suse 12](#installing-the-drivers-on-suse-12) - [Installing the drivers on macOS El Capitan, Sierra, High Sierra and Mojave](#installing-the-drivers-on-macos-el-capitan-sierra-high-sierra-and-mojave) -## Installing the drivers on Ubuntu 16.04 and 18.04 +## Installing the drivers on Ubuntu 16.04, 18.04 and 18.10 > [!NOTE] > To install PHP 7.0 or 7.1, replace 7.2 with 7.0 or 7.1 in the following commands. > For Ubuntu 18.04, the step to add the ondrej repository is not required unless -> PHP 7.0 or 7.1 is needed. However, installing PHP 7.0 or 7.1 in Ubuntu 18.04 may -> not work as packages from the ondrej repository come with dependencies that may -> conflict with a base Ubuntu 18.04 install. +> PHP 7.0 or 7.1 is needed. However, installing PHP 7.0 or 7.1 in Ubuntu 18.04 or 18.10 +> may not work as packages from the ondrej repository come with dependencies that may +> conflict with a base Ubuntu 18.04 or 18.10 install. ### Step 1. Install PHP ``` sudo su -add-apt-repository ppa:ondrej/php -y +# The following step is required for Ubuntu 16.04 only +add-apt-repository ppa:ondrej/php -y apt-get update apt-get install php7.2 php7.2-dev php7.2-xml -y --allow-unauthenticated ``` ### Step 2. Install prerequisites -Install the ODBC driver for Ubuntu by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). +Install the ODBC driver for Ubuntu by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). + +For Ubuntu 18.10, follow the above steps for Ubuntu 18.04 except replace `18.04` by `18.10` and download ODBC 17.3 preview [here](https://www.microsoft.com/en-us/download/details.aspx?id=57341). ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` From f4ad2ae1d4df544246cae7bb6b30ee353b3d4843 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 2 Nov 2018 14:34:27 -0700 Subject: [PATCH 06/10] Feature request 415 for pdo_sqlsrv (#873) --- source/pdo_sqlsrv/pdo_dbh.cpp | 10 +- source/pdo_sqlsrv/pdo_init.cpp | 1 + source/pdo_sqlsrv/pdo_stmt.cpp | 4 + source/pdo_sqlsrv/pdo_util.cpp | 8 + source/pdo_sqlsrv/php_pdo_sqlsrv.h | 5 +- source/shared/core_sqlsrv.h | 3 +- source/shared/core_stmt.cpp | 70 ++-- source/shared/core_util.cpp | 19 - .../pdostatement_format_decimals.phpt | 390 ++++++++++++++++++ .../pdostatement_format_decimals_scales.phpt | 248 +++++++++++ .../sqlsrv_statement_format_decimals.phpt | 54 ++- 11 files changed, 745 insertions(+), 67 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt create mode 100644 test/functional/pdo_sqlsrv/pdostatement_format_decimals_scales.phpt diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index b424866e..bf25d5a8 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -80,7 +80,8 @@ enum PDO_STMT_OPTIONS { PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE, PDO_STMT_OPTION_EMULATE_PREPARES, PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, - PDO_STMT_OPTION_FETCHES_DATETIME_TYPE + PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, + PDO_STMT_OPTION_FORMAT_DECIMALS }; // List of all the statement options supported by this driver. @@ -95,6 +96,7 @@ const stmt_option PDO_STMT_OPTS[] = { { NULL, 0, PDO_STMT_OPTION_EMULATE_PREPARES, std::unique_ptr( new stmt_option_emulate_prepares ) }, { NULL, 0, PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, std::unique_ptr( new stmt_option_fetch_numeric ) }, { NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr( new stmt_option_fetch_datetime ) }, + { NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr( new stmt_option_format_decimals ) }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -1095,6 +1097,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_FORMAT_DECIMALS: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1153,6 +1156,7 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_FORMAT_DECIMALS: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1586,6 +1590,10 @@ void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; break; + case SQLSRV_ATTR_FORMAT_DECIMALS: + option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; + break; + default: CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { throw core::CoreException(); diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index a5fcbdd0..6d47cf5b 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -286,6 +286,7 @@ namespace { { "SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE", SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE }, { "SQLSRV_ATTR_FETCHES_NUMERIC_TYPE", SQLSRV_ATTR_FETCHES_NUMERIC_TYPE }, { "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE }, + { "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS }, // used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int, // PDO::PARAM_STR uses the size of the string in the variable diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 697ca40e..de1fc882 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -882,6 +882,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In driver_stmt->fetch_datetime = ( zend_is_true( val )) ? true : false; break; + case SQLSRV_ATTR_FORMAT_DECIMALS: + core_sqlsrv_set_format_decimals(driver_stmt, val TSRMLS_CC); + break; + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index dee2e3e0..0295b406 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -437,6 +437,14 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, { IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false} }, + { + SQLSRV_ERROR_INVALID_FORMAT_DECIMALS, + { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -92, false} + }, + { + SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, + { IMSSP, (SQLCHAR*) "For formatting decimal data values, %1!d! is out of range. Expected an integer from 0 to 38, inclusive.", -93, true} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 3b13953a..ced89eee 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -41,14 +41,15 @@ extern "C" { // sqlsrv driver specific PDO attributes enum PDO_SQLSRV_ATTR { - // Currently there are only three custom attributes for this driver. + // The custom attributes for this driver: SQLSRV_ATTR_ENCODING = PDO_ATTR_DRIVER_SPECIFIC, SQLSRV_ATTR_QUERY_TIMEOUT, SQLSRV_ATTR_DIRECT_QUERY, SQLSRV_ATTR_CURSOR_SCROLL_TYPE, SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, - SQLSRV_ATTR_FETCHES_DATETIME_TYPE + SQLSRV_ATTR_FETCHES_DATETIME_TYPE, + SQLSRV_ATTR_FORMAT_DECIMALS }; // valid set of values for TransactionIsolation connection option diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 59b5afcd..91be215d 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1527,7 +1527,7 @@ void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit TSRMLS_DC ); - +void core_sqlsrv_set_format_decimals(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC); //********************************************************************************************************************************* // Result Set @@ -1707,7 +1707,6 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { // utility functions shared by multiple callers across files bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_updates_z_(len) char** string, _Inout_ SQLLEN& len); -bool convert_zval_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _Inout_ zval* value_z, _Inout_ SQLLEN& len); bool validate_string( _In_ char* string, _In_ SQLLEN& len); bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(cchInLen) const SQLWCHAR* inString, _In_ SQLINTEGER cchInLen, _Inout_updates_bytes_(cchOutLen) char** outString, _Out_ SQLLEN& cchOutLen ); SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 58ec4520..206fe11d 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -1258,6 +1258,26 @@ void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout } } +void core_sqlsrv_set_format_decimals(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC) +{ + try { + // first check if the input is an integer + CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) { + throw core::CoreException(); + } + + zend_long format_decimals = Z_LVAL_P(value_z); + CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) { + throw core::CoreException(); + } + + stmt->num_decimals = static_cast(format_decimals); + } + catch( core::CoreException& ) { + throw; + } +} + void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ) { TSRMLS_C; @@ -1427,17 +1447,7 @@ void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC ) { - // first check if the input is an integer - CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) { - throw core::CoreException(); - } - - zend_long format_decimals = Z_LVAL_P(value_z); - CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) { - throw core::CoreException(); - } - - stmt->num_decimals = static_cast(format_decimals); + core_sqlsrv_set_format_decimals(stmt, value_z TSRMLS_CC); } // internal function to release the active stream. Called by each main API function @@ -2293,27 +2303,39 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) str_len = output_param->original_buffer_len - null_size; } - // if it's not in the 8 bit encodings, then it's in UTF-16 - if( output_param->encoding != SQLSRV_ENCODING_CHAR && output_param->encoding != SQLSRV_ENCODING_BINARY ) { - bool converted = convert_zval_string_from_utf16(output_param->encoding, value_z, str_len); - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) { - throw core::CoreException(); - } - } - else if( output_param->encoding == SQLSRV_ENCODING_BINARY && str_len < output_param->original_buffer_len ) { + if (output_param->encoding == SQLSRV_ENCODING_BINARY) { // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated // so we do that here if the length of the returned data is less than the original allocation. The // original allocation null terminates the buffer already. - str[str_len] = '\0'; + if (str_len < output_param->original_buffer_len) { + str[str_len] = '\0'; + } core::sqlsrv_zval_stringl(value_z, str, str_len); } else { SQLSMALLINT decimal_digits = output_param->getDecimalDigits(); - if (stmt->num_decimals >= 0 && decimal_digits >= 0) { - format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len); - } - core::sqlsrv_zval_stringl(value_z, str, str_len); + if (output_param->encoding != SQLSRV_ENCODING_CHAR) { + char* outString = NULL; + SQLLEN outLen = 0; + bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen ); + CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + + if (stmt->num_decimals >= 0 && decimal_digits >= 0) { + format_decimal_numbers(stmt->num_decimals, decimal_digits, outString, &outLen); + } + core::sqlsrv_zval_stringl(value_z, outString, outLen); + sqlsrv_free(outString); + } + else { + if (stmt->num_decimals >= 0 && decimal_digits >= 0) { + format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len); + } + + core::sqlsrv_zval_stringl(value_z, str, str_len); + } } } break; diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index ca097a24..03675a08 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -91,25 +91,6 @@ bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_up return result; } -bool convert_zval_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _Inout_ zval* value_z, _Inout_ SQLLEN& len) -{ - char* string = Z_STRVAL_P(value_z); - - if( validate_string(string, len)) { - return true; - } - - char* outString = NULL; - SQLLEN outLen = 0; - bool result = convert_string_from_utf16( encoding, reinterpret_cast(string), int(len / sizeof(SQLWCHAR)), &outString, outLen ); - if( result ) { - core::sqlsrv_zval_stringl( value_z, outString, outLen ); - sqlsrv_free( outString ); - len = outLen; - } - return result; -} - bool validate_string( _In_ char* string, _In_ SQLLEN& len ) { SQLSRV_ASSERT(string != NULL, "String must be specified"); diff --git a/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt b/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt new file mode 100644 index 00000000..b4410e27 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt @@ -0,0 +1,390 @@ +--TEST-- +Test statement attribute PDO::SQLSRV_ATTR_FORMAT_DECIMALS for decimal types +--DESCRIPTION-- +Test statement attribute PDO::SQLSRV_ATTR_FORMAT_DECIMALS for decimal or +money types (feature request issue 415), which are always fetched as strings +to preserve accuracy and precision, unlike other primitive numeric types, +where there is an option to retrieve them as numbers. + +This attribute expects an integer value from the range [0,38], the money or +decimal types in the fetched result set can be formatted. + +No effect on other operations like insertion or update. + +1. By default, data will be returned with the original precision and scale +2. The data column original scale still takes precedence – for example, if the user +specifies 3 decimal digits for a column of decimal(5,2), the result still shows only 2 +decimals to the right of the dot +3. After formatting, the missing leading zeroes will be padded +4. The underlying data will not be altered, but formatted results may likely be rounded +up (e.g. .2954 will be displayed as 0.30 if the user wants only two decimals) +5. Do not support output params +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +getMessage(), $expected) === false) { + print_r($exception->getMessage()); + echo "\n"; + } +} + +function testPdoAttribute($conn, $setAttr) +{ + // Expects exception because PDO::SQLSRV_ATTR_FORMAT_DECIMALS + // is a statement level attribute + try { + $res = true; + if ($setAttr) { + $res = $conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, 1); + } else { + $res = $conn->getAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS); + } + if ($res) { + echo "setAttribute at PDO level should have failed!\n"; + } + } catch (PdoException $e) { + if ($setAttr) { + $expected = 'The given attribute is only supported on the PDOStatement object.'; + } else { + $expected = 'driver does not support that attribute'; + } + + checkException($e, $expected); + } +} + +function testErrorCases($conn) +{ + $query = "SELECT 0.0001"; + + try { + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => 0.9); + $stmt = $conn->prepare($query, $options); + } catch (PdoException $e) { + $expected = 'Expected an integer to specify number of decimals to format the output values of decimal data types'; + checkException($e, $expected); + } + + try { + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => 100); + $stmt = $conn->prepare($query, $options); + } catch (PdoException $e) { + $expected = 'For formatting decimal data values, 100 is out of range. Expected an integer from 0 to 38, inclusive.'; + checkException($e, $expected); + } +} + +function verifyMoneyValues($conn, $query, $values, $numDigits) +{ + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $results = $stmt->fetch(PDO::FETCH_NUM); + + trace("\nverifyMoneyValues:\n"); + for ($i = 0; $i < count($values); $i++) { + $value = number_format($values[$i], $numDigits); + trace("$results[$i], $value\n"); + + if ($value !== $results[$i]) { + echo "testMoneyTypes: Expected $value but got $results[$i]\n"; + } + } +} + +function testFloatTypes($conn) +{ + // This test with the float types of various number of bits, which are retrieved + // as numbers by default. When fetched as strings, no formatting is done even with + // the statement option FormatDecimals set + $epsilon = 0.001; + $values = array(); + for ($i = 0; $i < 5; $i++) { + $n1 = rand(1, 100); + $n2 = rand(1, 100); + $neg = ($i % 2 == 0) ? -1 : 1; + + $n = $neg * $n1 / $n2; + array_push($values, $n); + } + + $query = "SELECT CONVERT(float(1), $values[0]), + CONVERT(float(12), $values[1]), + CONVERT(float(24), $values[2]), + CONVERT(float(36), $values[3]), + CONVERT(float(53), $values[4])"; + $stmt = $conn->query($query); + $floats = $stmt->fetch(PDO::FETCH_NUM); + unset($stmt); + + // Set PDO::SQLSRV_ATTR_FORMAT_DECIMALS to 2 should + // have no effect on floating point numbers + $numDigits = 2; + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits); + $stmt = $conn->prepare($query, $options); + + // By default the floating point numbers are fetched as strings + for ($i = 0; $i < 5; $i++) { + $stmt->execute(); + $floatStr = $stmt->fetchColumn($i); + + $floatVal = floatVal($floats[$i]); + $floatVal1 = floatval($floatStr); + + trace("testFloatTypes: $floatVal1, $floatVal\n"); + + // Check if the numbers of decimal digits are the same + // It is highly unlikely but not impossible + $numbers = explode('.', $floatStr); + $len = strlen($numbers[1]); + if ($len == $numDigits && $floatVal1 != $floatVal) { + echo "Expected $floatVal but $floatVal1 returned. \n"; + } else { + $diff = abs($floatVal1 - $floatVal) / $floatVal; + if ($diff > $epsilon) { + echo "Expected $floatVal but $floatVal1 returned. \n"; + } + } + } +} + +function testMoneyTypes($conn) +{ + // With money and smallmoney types, which are essentially decimal types + // ODBC driver does not support Always Encrypted feature with money / smallmoney + $values = array('24.559', '0', '-0.946', '0.2985', '-99.675', '79.995'); + $defaults = array('24.5590', '.0000', '-.9460', '.2985', '-99.6750', '79.9950'); + + $query = "SELECT CONVERT(smallmoney, $values[0]), + CONVERT(money, $values[1]), + CONVERT(smallmoney, $values[2]), + CONVERT(money, $values[3]), + CONVERT(smallmoney, $values[4]), + CONVERT(money, $values[5])"; + + $stmt = $conn->query($query); + $results = $stmt->fetch(PDO::FETCH_NUM); + for ($i = 0; $i < count($values); $i++) { + if ($defaults[$i] !== $results[$i]) { + echo "testMoneyTypes: Expected $defaults[$i] but got $results[$i]\n"; + } + } + unset($stmt); + + // Set PDO::SQLSRV_ATTR_FORMAT_DECIMALS to 0 then 2 + verifyMoneyValues($conn, $query, $values, 0); + verifyMoneyValues($conn, $query, $values, 2); +} + +function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal = -1) +{ + $matched = false; + if ($actual === $input) { + $matched = true; + trace("$actual, $input\n"); + } else { + // When $formatDecimal is negative, that means no formatting done + // Otherwise, if $formatDecimal > $fieldScale, will show $fieldScale decimal digits + if ($formatDecimal >= 0) { + $numDecimals = ($formatDecimal > $fieldScale) ? $fieldScale : $formatDecimal; + } else { + $numDecimals = $fieldScale; + } + $expected = number_format($input, $numDecimals); + trace("$actual, $expected\n"); + if ($actual === $expected) { + $matched = true; + } else { + echo "For $column: expected $expected but the value is $actual\n"; + } + } + return $matched; +} + +function testNoOption($conn, $tableName, $inputs, $columns) +{ + // Without the statement option, should return decimal values as they are + $query = "SELECT * FROM $tableName"; + $stmt = $conn->query($query); + + // Compare values + $results = $stmt->fetch(PDO::FETCH_NUM); + trace("\ntestNoOption:\n"); + for ($i = 0; $i < count($inputs); $i++) { + compareNumbers($results[$i], $inputs[$i], $columns[$i], $i); + } +} + +function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $withBuffer) +{ + // Decimal values should return decimal digits based on the valid statement + // option PDO::SQLSRV_ATTR_FORMAT_DECIMALS + $query = "SELECT * FROM $tableName"; + if ($withBuffer){ + $options = array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, + PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED, + PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $formatDecimal); + } else { + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $formatDecimal); + } + + $size = count($inputs); + $stmt = $conn->prepare($query, $options); + + // Fetch by getting one field at a time + trace("\ntestStmtOption: $formatDecimal and buffered $withBuffer\n"); + for ($i = 0; $i < $size; $i++) { + $stmt->execute(); + + $stmt->bindColumn($columns[$i], $field); + $result = $stmt->fetch(PDO::FETCH_BOUND); + + compareNumbers($field, $inputs[$i], $columns[$i], $i, $formatDecimal); + } +} + +function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $inout) +{ + $outString = ''; + $numDigits = 2; + + $outSql = getCallProcSqlPlaceholders($storedProcName, 1); + + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits); + $stmt = $conn->prepare($outSql, $options); + + $len = 1024; + if ($inout) { + $paramType = PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT; + + // For inout parameters the input type should match the output one + $outString = '0.0'; + } else { + $paramType = PDO::PARAM_STR; + } + + $stmt->bindParam(1, $outString, $paramType, $len); + $stmt->execute(); + + // The output param value should be the same as the input value, + // unaffected by the statement attr PDO::SQLSRV_ATTR_FORMAT_DECIMALS, + // unless ColumnEncryption is enabled, in which case the driver is able + // to derive the decimal type + if (isAEConnected()) { + trace("\ngetOutputParam ($inout) with AE:\n"); + $column = 'outputParamAE'; + compareNumbers($outString, $inputValue, $column, $scale, $numDigits); + } else { + trace("\ngetOutputParam ($inout) without AE:\n"); + $column = 'outputParam'; + compareNumbers($outString, $inputValue, $column, $scale); + } +} + +function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inout = false) +{ + for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) { + // Create the stored procedure first + $storedProcName = "spFormatDecimals" . $i; + $procArgs = "@col $dataTypes[$i] OUTPUT"; + $procCode = "SELECT @col = $columns[$i] FROM $tableName"; + createProc($conn, $storedProcName, $procArgs, $procCode); + + // Call stored procedure to retrieve output param + getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $inout); + + dropProc($conn, $storedProcName); + } +} + +try { + // This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION + $conn = connect(); + + // Test some error conditions + testPdoAttribute($conn, true); + testPdoAttribute($conn, false); + testErrorCases($conn); + + // First test with money types + testMoneyTypes($conn); + + // Also test using regular floats + testFloatTypes($conn); + + // Create the test table of decimal / numeric data columns + $tableName = 'pdoFormatDecimals'; + + $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6'); + $dataTypes = array('decimal(3,0)', 'decimal(4,1)', 'decimal(5,2)', 'numeric(6,3)', 'numeric(7,4)', 'numeric(8, 5)'); + + $colMeta = array(new ColumnMeta($dataTypes[0], $columns[0]), + new ColumnMeta($dataTypes[1], $columns[1]), + new ColumnMeta($dataTypes[2], $columns[2]), + new ColumnMeta($dataTypes[3], $columns[3]), + new ColumnMeta($dataTypes[4], $columns[4]), + new ColumnMeta($dataTypes[5], $columns[5])); + createTable($conn, $tableName, $colMeta); + + // Generate random input values based on precision and scale + trace("\nGenerating random input values: \n"); + $values = array(); + $max = 1; + for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) { + // First get a random number + $n = rand(1, 6); + $neg = ($n % 2 == 0) ? -1 : 1; + + // $n1 may or may not be negative + $n1 = rand(0, 1000) * $neg; + + if ($s > 0) { + $max *= 10; + $n2 = rand(0, $max); + $number = sprintf("%d.%d", $n1, $n2); + } else { + $number = sprintf("%d", $n1); + } + + trace("$s: $number\n"); + array_push($values, $number); + } + + $query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + $stmt->bindParam($i+1, $values[$i]); + } + $stmt->execute(); + + testNoOption($conn, $tableName, $values, $columns, true); + + // Now try with setting number decimals to 3 then 2 + testStmtOption($conn, $tableName, $values, $columns, 3, false); + testStmtOption($conn, $tableName, $values, $columns, 3, true); + + testStmtOption($conn, $tableName, $values, $columns, 2, false); + testStmtOption($conn, $tableName, $values, $columns, 2, true); + + // Test output parameters + testOutputParam($conn, $tableName, $values, $columns, $dataTypes); + testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true); + + dropTable($conn, $tableName); + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdostatement_format_decimals_scales.phpt b/test/functional/pdo_sqlsrv/pdostatement_format_decimals_scales.phpt new file mode 100644 index 00000000..a8a40d48 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdostatement_format_decimals_scales.phpt @@ -0,0 +1,248 @@ +--TEST-- +Test various precisions of formatting decimal data output values (feature request issue 415) +--DESCRIPTION-- +In SQL Server, the maximum allowed precision is 38. The scale can range from 0 up to the +defined precision. Generate a long numeric string and get rid of the last digit to make it a +39-digit-string. Then replace one digit at a time with a dot '.' to make it a decimal +input string for testing with various scales. +For example, +string(39) ".23456789012345678901234567890123456789" +string(39) "1.3456789012345678901234567890123456789" +string(39) "12.456789012345678901234567890123456789" +string(39) "123.56789012345678901234567890123456789" +string(39) "1234.6789012345678901234567890123456789" +string(39) "12345.789012345678901234567890123456789" +... ... +string(39) "1234567890123456789012345678901234.6789" +string(39) "12345678901234567890123456789012345.789" +string(39) "123456789012345678901234567890123456.89" +string(39) "1234567890123456789012345678901234567.9" +string(38) "12345678901234567890123456789012345678" + +Note: PHP number_format() will not be used for verification in this test +because the function starts losing accuracy with large number of precisions / scales. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $digits)); + + // Restore the $i-th digit with its original digit + $digits[$i] = $d; + } + + $stmt = insertRow($conn, $tableName, $inputData); + unset($stmt); + + return $inputData; +} + +function verifyNoDecimals($value, $input, $round) +{ + global $prec, $dot; + + // Use PHP explode() to separate the input string into an array + $parts = explode($dot, $input); + $len = strlen($parts[0]); + if ($len == 0) { + // The original input string is missing a leading zero + $parts[0] = '0'; + } + + // No need to worry about carry over for the input data of this test + // Check the first digit of $parts[1] + if ($len < $prec) { + // Only need to round up when $len < $prec + $ch = $parts[1][0]; + + // Round the last digit of $parts[0] if $ch is '5' or above + if ($ch >= '5') { + $len = strlen($parts[0]); + $parts[0][$len-1] = $parts[0][$len-1] + 1 + '0'; + } + } + + // No decimal digits left in the expected string + $expected = $parts[0]; + if ($value !== $expected) { + echo "Round $round scale 0: expected $expected but returned $value\n"; + } +} + +function verifyWithDecimals($value, $input, $round, $scale) +{ + global $dot; + + // Use PHP explode() to separate the input string into an array + $parts = explode($dot, $input); + if (strlen($parts[0]) == 0) { + // The original input string is missing a leading zero + $parts[0] = '0'; + } + + // No need to worry about carry over for the input data of this test + // Check the digit at the position $scale of $parts[1] + $len = strlen($parts[1]); + if ($scale < $len) { + // Only need to round up when $scale < $len + $ch = $parts[1][$scale]; + + // Round the previous digit if $ch is '5' or above + if ($ch >= '5') { + $parts[1][$scale-1] = $parts[1][$scale-1] + 1 + '0'; + } + } + + // Use substr() to get up to $scale + $parts[1] = substr($parts[1], 0, $scale); + + // Join the array elements together + $expected = implode($dot, $parts); + if ($value !== $expected) { + echo "Round $round scale $scale: expected $expected but returned $value\n"; + } +} + +/**** +The function testVariousScales() will fetch one column at a time, using scale from +0 up to the maximum scale allowed for that column type. + +For example, for column of type decimal(38,4), the input string is +1234567890123456789012345678901234.6789 + +When fetching data, using scale from 0 to 4, the following values are expected to return: +1234567890123456789012345678901235 +1234567890123456789012345678901234.7 +1234567890123456789012345678901234.68 +1234567890123456789012345678901234.679 +1234567890123456789012345678901234.6789 + +For example, for column of type decimal(38,6), the input string is +12345678901234567890123456789012.456789 + +When fetching data, using scale from 0 to 6, the following values are expected to return: +12345678901234567890123456789012 +12345678901234567890123456789012.5 +12345678901234567890123456789012.46 +12345678901234567890123456789012.457 +12345678901234567890123456789012.4568 +12345678901234567890123456789012.45679 +12345678901234567890123456789012.456789 + +etc. +****/ +function testVariousScales($conn, $tableName, $inputData) +{ + global $prec; + $max = $prec + 1; + + for ($i = 0; $i < $max; $i++) { + $scale = $prec - $i; + $column = "col_$scale"; + + $query = "SELECT $column as col1 FROM $tableName"; + $input = $inputData[$column]; + + // Default case: the fetched value should be the same as the corresponding input + $stmt = $conn->query($query); + if ($obj = $stmt->fetchObject()) { + trace("\n$obj->col1\n"); + if ($obj->col1 !== $input) { + echo "default case: expected $input but returned $obj->col1\n"; + } + } else { + echo "In testVariousScales: fetchObject failed\n"; + } + + // Next, format how many decimal digits to be displayed + $query = "SELECT $column FROM $tableName"; + for ($j = 0; $j <= $scale; $j++) { + $options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $j); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + + $stmt->bindColumn($column, $value); + if ($stmt->fetch(PDO::FETCH_BOUND)) { + trace("$value\n"); + if ($j == 0) { + verifyNoDecimals($value, $input, $i); + } else { + verifyWithDecimals($value, $input, $i, $j); + } + } else { + echo "Round $i scale $j: fetch failed\n"; + } + } + } +} + +try { + // This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION + $conn = connect(); + + $tableName = createTestTable($conn); + $inputData = insertTestData($conn, $tableName); + testVariousScales($conn, $tableName, $inputData); + + dropTable($conn, $tableName); + + echo "Done\n"; + + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt index 7e9e4b24..23ddbba9 100644 --- a/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt +++ b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt @@ -107,17 +107,21 @@ function testFloatTypes($conn) if (sqlsrv_fetch($stmt)) { for ($i = 0; $i < count($values); $i++) { $floatStr = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + $floatVal = floatval($floatStr); + + // Check if the numbers of decimal digits are the same + // It is highly unlikely but not impossible $numbers = explode('.', $floatStr); $len = strlen($numbers[1]); - if ($len == $numDigits) { - // This is highly unlikely - var_dump($floatStr); - } - $floatVal = floatval($floatStr); - $diff = abs($floatVal - $floats[$i]) / $floats[$i]; - if ($diff > $epsilon) { - var_dump($diff); + if ($len == $numDigits && $floatVal != $floats[$i]) { + echo "Expected $floats[$i] but returned "; var_dump($floatVal); + } else { + $diff = abs($floatVal - $floats[$i]) / $floats[$i]; + if ($diff > $epsilon) { + echo "Expected $floats[$i] but returned "; + var_dump($floatVal); + } } } } else { @@ -216,17 +220,31 @@ function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $w } } -function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale) +function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $inout) { $outString = ''; $numDigits = 2; + $dir = SQLSRV_PARAM_OUT; - // Derive the sqlsrv type SQLSRV_SQLTYPE_DECIMAL($prec, $scale) - $sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale); + // The output param value should be the same as the input value, + // unaffected by the statement attr FormatDecimals, unless + // ColumnEncryption is enabled, in which case the driver is able + // to derive the decimal type. Another workaround is to specify + // the SQLSRV_SQLTYPE_DECIMAL type with the correct precision and scale + $sqlType = null; + if (!AE\isColEncrypted()) { + $sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale); + } + + // For inout parameters the input type should match the output one + if ($inout) { + $dir = SQLSRV_PARAM_INOUT; + $outString = '0.0'; + } $outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1); $stmt = sqlsrv_prepare($conn, $outSql, - array(array(&$outString, SQLSRV_PARAM_OUT, null, $sqlType)), + array(array(&$outString, $dir, null, $sqlType)), array('FormatDecimals' => $numDigits)); if (!$stmt) { fatalError("getOutputParam: failed when preparing to call $storedProcName"); @@ -242,14 +260,11 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale) sqlsrv_free_stmt($stmt); if (!AE\isColEncrypted()) { - // Get output param without specifying sqlsrv type, and the returned value will - // be a regular string -- its value should be the same as the input value, - // unaffected by the statement option FormatDecimals // With ColumnEncryption enabled, the driver is able to derive the decimal type, // so skip this part of the test - $outString2 = ''; + $outString2 = $inout ? '0.0' : ''; $stmt = sqlsrv_prepare($conn, $outSql, - array(array(&$outString2, SQLSRV_PARAM_OUT)), + array(array(&$outString2, $dir)), array('FormatDecimals' => $numDigits)); if (!$stmt) { fatalError("getOutputParam2: failed when preparing to call $storedProcName"); @@ -264,7 +279,7 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale) } } -function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes) +function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inout = false) { for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) { // Create the stored procedure first @@ -274,7 +289,7 @@ function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes) createProc($conn, $storedProcName, $procArgs, $procCode); // Call stored procedure to retrieve output param - getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i); + getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $inout); dropProc($conn, $storedProcName); } @@ -360,6 +375,7 @@ testStmtOption($conn, $tableName, $values, $columns, 2, true); // Test output parameters testOutputParam($conn, $tableName, $values, $columns, $dataTypes); +testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true); dropTable($conn, $tableName); sqlsrv_close($conn); From 3679b48df2e307b0a2f1f833450963f5e8ef93ed Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Wed, 7 Nov 2018 16:37:11 -0800 Subject: [PATCH 07/10] Skipped some tests when running against Azure (#874) --- test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt | 2 +- test/functional/pdo_sqlsrv/issue_52_pdo.phpt | 2 +- .../pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt | 3 ++- test/functional/sqlsrv/MsCommon.inc | 12 ++++++++++++ test/functional/sqlsrv/TC81_MemoryCheck.phpt | 3 ++- test/functional/sqlsrv/issue_52.phpt | 2 +- .../sqlsrv/sqlsrv_ae_azure_key_vault_keywords.phpt | 3 ++- .../sqlsrv/sqlsrv_azure_ad_authentication.phpt | 2 +- 8 files changed, 22 insertions(+), 7 deletions(-) diff --git a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt index 8cc64838..9fc7cd95 100644 --- a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt +++ b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt @@ -6,7 +6,7 @@ emalloc (which only allocate memory in the memory space allocated for the PHP pr --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- + --FILE-- + --FILE-- + --FILE-- + --FILE-- + --FILE-- $azureUsername, "PWD"=>$azurePassword, - "Authentication"=>'ActiveDirectoryPassword', "TrustServerCertificate"=>true ); + "Authentication"=>'ActiveDirectoryPassword', "TrustServerCertificate"=>false ); $conn = sqlsrv_connect( $azureServer, $connectionInfo ); if( $conn === false ) From 69e82080ea19f633d56c49b983984f8828fc675c Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Tue, 13 Nov 2018 15:33:07 -0800 Subject: [PATCH 08/10] Modified config files to add the compiler flag, /Qspectre (#878) --- appveyor.yml | 2 +- source/pdo_sqlsrv/config.w32 | 9 ++++++++- source/sqlsrv/config.w32 | 11 +++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4a0fcbdd..ee9f2eb3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,7 +24,7 @@ environment: SQL_INSTANCE: SQL2017 PHP_VC: 15 PHP_MAJOR_VER: 7.2 - PHP_MINOR_VER: latest + PHP_MINOR_VER: 11 PHP_EXE_PATH: x64\Release_TS THREAD: ts platform: x64 diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index cd6e3a06..8b8e4f29 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -37,8 +37,15 @@ if( PHP_PDO_SQLSRV != "no" ) { if (PHP_DEBUG != "yes") ADD_FLAG( "CFLAGS_PDO_SQLSRV", "/guard:cf /O2" ); ADD_FLAG( "CFLAGS_PDO_SQLSRV", "/D ZEND_WIN32_FORCE_INLINE" ); if (VCVERS >= 1913) { + ADD_FLAG("LDFLAGS_PDO_SQLSRV", "/d2:-guardspecload"); ADD_FLAG("CFLAGS_PDO_SQLSRV", "/Qspectre"); - } + } else if (VCVERS == 1900) { + var subver1900 = probe_binary(PHP_CL).substr(6); + if (subver1900 >= 24241) { + ADD_FLAG("LDFLAGS_PDO_SQLSRV", "/d2:-guardspecload"); + ADD_FLAG('CFLAGS_PDO_SQLSRV', "/Qspectre"); + } + } ADD_EXTENSION_DEP('pdo_sqlsrv', 'pdo'); EXTENSION("pdo_sqlsrv", pdo_sqlsrv_src_class, PHP_PDO_SQLSRV_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); } else { diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index b6c2b1b1..e4cd19d0 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -36,11 +36,18 @@ if( PHP_SQLSRV != "no" ) { ADD_FLAG( "CFLAGS_SQLSRV", "/GS" ); ADD_FLAG( "CFLAGS_SQLSRV", "/Zi" ); if (VCVERS >= 1913) { + ADD_FLAG("LDFLAGS_SQLSRV", "/d2:-guardspecload"); ADD_FLAG("CFLAGS_SQLSRV", "/Qspectre"); - } + } else if (VCVERS == 1900) { + var subver1900 = probe_binary(PHP_CL).substr(6); + if (subver1900 >= 24241) { + ADD_FLAG("LDFLAGS_SQLSRV", "/d2:-guardspecload"); + ADD_FLAG('CFLAGS_SQLSRV', "/Qspectre"); + } + } if (PHP_DEBUG != "yes") ADD_FLAG( "CFLAGS_SQLSRV", "/guard:cf /O2" ); EXTENSION("sqlsrv", sqlsrv_src_class , PHP_SQLSRV_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); } else { WARNING("sqlsrv not enabled; libraries and headers not found"); - } + } } From d51f6db9c1a5ac57d71bb305892c7482cf8916f0 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Wed, 14 Nov 2018 12:19:29 -0800 Subject: [PATCH 09/10] Merge the commit from master re survey image link (#880) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19a5d88c..5d1422cc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.1+ with improv Thank you for taking the time to participate in our last survey. You can continue to help us improve by letting us know how we are doing and how you use PHP by taking our December pulse survey: - + ### Status of Most Recent Builds | AppVeyor (Windows) | Travis CI (Linux) | Coverage (Windows) | Coverage (Linux) | From 78911f4697805fae0265f9ff07981f8bba9bf590 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 16 Nov 2018 15:03:53 -0800 Subject: [PATCH 10/10] Fixed the flaws of decimal tests and added more debugging (#879) --- .../pdostatement_format_decimals.phpt | 17 +++++++++++------ .../sqlsrv_statement_format_decimals.phpt | 13 ++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt b/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt index b4410e27..ff29a808 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt @@ -188,21 +188,26 @@ function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal = $matched = false; if ($actual === $input) { $matched = true; - trace("$actual, $input\n"); + trace("Matched: $actual, $input\n"); } else { // When $formatDecimal is negative, that means no formatting done // Otherwise, if $formatDecimal > $fieldScale, will show $fieldScale decimal digits if ($formatDecimal >= 0) { $numDecimals = ($formatDecimal > $fieldScale) ? $fieldScale : $formatDecimal; + $expected = number_format($input, $numDecimals); } else { - $numDecimals = $fieldScale; + $expected = number_format($input, $fieldScale); + if (abs($input) < 1) { + // Since no formatting, the leading zero should not be there + trace("Drop leading zero of $input--"); + $expected = str_replace('0.', '.', $expected); + } } - $expected = number_format($input, $numDecimals); - trace("$actual, $expected\n"); + trace("With number_format: $actual, $expected\n"); if ($actual === $expected) { $matched = true; } else { - echo "For $column: expected $expected but the value is $actual\n"; + echo "For $column ($formatDecimal): expected $expected ($input) but the value is $actual\n"; } } return $matched; @@ -265,7 +270,7 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $ino $paramType = PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT; // For inout parameters the input type should match the output one - $outString = '0.0'; + $outString = '0.0'; } else { $paramType = PDO::PARAM_STR; } diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt index 23ddbba9..223e4fb6 100644 --- a/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt +++ b/test/functional/sqlsrv/sqlsrv_statement_format_decimals.phpt @@ -30,19 +30,26 @@ function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal = $matched = false; if ($actual === $input) { $matched = true; + trace("Matched: $actual, $input\n"); } else { // When $formatDecimal is negative, that means no formatting done // Otherwise, if $formatDecimal > $fieldScale, will show $fieldScale decimal digits if ($formatDecimal >= 0) { $numDecimals = ($formatDecimal > $fieldScale) ? $fieldScale : $formatDecimal; + $expected = number_format($input, $numDecimals); } else { - $numDecimals = $fieldScale; + $expected = number_format($input, $fieldScale); + if (abs($input) < 1) { + // Since no formatting, the leading zero should not be there + trace("Drop leading zero of $input--"); + $expected = str_replace('0.', '.', $expected); + } } - $expected = number_format($input, $numDecimals); + trace("With number_format: $actual, $expected\n"); if ($actual === $expected) { $matched = true; } else { - echo "For $column: expected $expected but the value is $actual\n"; + echo "For $column ($formatDecimal): expected $expected ($input) but the value is $actual\n"; } } return $matched;