Decimal places for money types only (#886)

This commit is contained in:
Jenny Tam 2018-11-27 17:18:38 -08:00 committed by GitHub
parent 8e6c181c59
commit 76c595fc2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1240 additions and 969 deletions

View file

@ -81,7 +81,8 @@ enum PDO_STMT_OPTIONS {
PDO_STMT_OPTION_EMULATE_PREPARES,
PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE,
PDO_STMT_OPTION_FETCHES_DATETIME_TYPE,
PDO_STMT_OPTION_FORMAT_DECIMALS
PDO_STMT_OPTION_FORMAT_DECIMALS,
PDO_STMT_OPTION_DECIMAL_PLACES
};
// List of all the statement options supported by this driver.
@ -97,6 +98,7 @@ const stmt_option PDO_STMT_OPTS[] = {
{ NULL, 0, PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, std::unique_ptr<stmt_option_fetch_numeric>( new stmt_option_fetch_numeric ) },
{ NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr<stmt_option_fetch_datetime>( new stmt_option_fetch_datetime ) },
{ NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr<stmt_option_format_decimals>( new stmt_option_format_decimals ) },
{ NULL, 0, PDO_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr<stmt_option_decimal_places>( new stmt_option_decimal_places ) },
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
};
@ -500,7 +502,9 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo
query_timeout( QUERY_TIMEOUT_INVALID ),
client_buffer_max_size( PDO_SQLSRV_G( client_buffer_max_size )),
fetch_numeric( false ),
fetch_datetime( false )
fetch_datetime( false ),
format_decimals( false ),
decimal_places( NO_CHANGE_DECIMAL_PLACES )
{
if( client_buffer_max_size < 0 ) {
client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT;
@ -1069,7 +1073,28 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
case SQLSRV_ATTR_FETCHES_DATETIME_TYPE:
driver_dbh->fetch_datetime = (zend_is_true(val)) ? true : false;
break;
case SQLSRV_ATTR_FORMAT_DECIMALS:
driver_dbh->format_decimals = (zend_is_true(val)) ? true : false;
break;
case SQLSRV_ATTR_DECIMAL_PLACES:
{
// first check if the input is an integer
if (Z_TYPE_P(val) != IS_LONG) {
THROW_PDO_ERROR(driver_dbh, SQLSRV_ERROR_INVALID_DECIMAL_PLACES);
}
zend_long decimal_places = Z_LVAL_P(val);
if (decimal_places < 0 || decimal_places > SQL_SERVER_MAX_MONEY_SCALE) {
// ignore decimal_places as this is out of range
decimal_places = NO_CHANGE_DECIMAL_PLACES;
}
driver_dbh->decimal_places = static_cast<short>(decimal_places);
}
break;
// Not supported
case PDO_ATTR_FETCH_TABLE_NAMES:
case PDO_ATTR_FETCH_CATALOG_NAMES:
@ -1097,7 +1122,6 @@ 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 );
}
@ -1156,7 +1180,6 @@ 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 );
}
@ -1229,6 +1252,18 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
break;
}
case SQLSRV_ATTR_FORMAT_DECIMALS:
{
ZVAL_BOOL( return_value, driver_dbh->format_decimals );
break;
}
case SQLSRV_ATTR_DECIMAL_PLACES:
{
ZVAL_LONG( return_value, driver_dbh->decimal_places );
break;
}
default:
{
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_INVALID_DBH_ATTR );
@ -1594,6 +1629,10 @@ void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_
option_key = PDO_STMT_OPTION_FORMAT_DECIMALS;
break;
case SQLSRV_ATTR_DECIMAL_PLACES:
option_key = PDO_STMT_OPTION_DECIMAL_PLACES;
break;
default:
CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) {
throw core::CoreException();

View file

@ -287,6 +287,7 @@ namespace {
{ "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 },
{ "SQLSRV_ATTR_DECIMAL_PLACES" , SQLSRV_ATTR_DECIMAL_PLACES },
// 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

View file

@ -883,7 +883,11 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In
break;
case SQLSRV_ATTR_FORMAT_DECIMALS:
core_sqlsrv_set_format_decimals(driver_stmt, val TSRMLS_CC);
driver_stmt->format_decimals = ( zend_is_true( val )) ? true : false;
break;
case SQLSRV_ATTR_DECIMAL_PLACES:
core_sqlsrv_set_decimal_places(driver_stmt, val TSRMLS_CC);
break;
default:
@ -973,6 +977,18 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In
break;
}
case SQLSRV_ATTR_FORMAT_DECIMALS:
{
ZVAL_BOOL( return_value, driver_stmt->format_decimals );
break;
}
case SQLSRV_ATTR_DECIMAL_PLACES:
{
ZVAL_LONG( return_value, driver_stmt->decimal_places );
break;
}
default:
THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR );
break;

View file

@ -438,13 +438,9 @@ pdo_error PDO_ERRORS[] = {
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false}
},
{
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
SQLSRV_ERROR_INVALID_DECIMAL_PLACES,
{ 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, {} }
};

View file

@ -49,7 +49,8 @@ enum PDO_SQLSRV_ATTR {
SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE,
SQLSRV_ATTR_FETCHES_NUMERIC_TYPE,
SQLSRV_ATTR_FETCHES_DATETIME_TYPE,
SQLSRV_ATTR_FORMAT_DECIMALS
SQLSRV_ATTR_FORMAT_DECIMALS,
SQLSRV_ATTR_DECIMAL_PLACES
};
// valid set of values for TransactionIsolation connection option
@ -206,6 +207,8 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn {
zend_long client_buffer_max_size;
bool fetch_numeric;
bool fetch_datetime;
bool format_decimals;
short decimal_places;
pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC );
};
@ -267,6 +270,8 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt {
direct_query = db->direct_query;
fetch_numeric = db->fetch_numeric;
fetch_datetime = db->fetch_datetime;
format_decimals = db->format_decimals;
decimal_places = db->decimal_places;
}
virtual ~pdo_sqlsrv_stmt( void );

View file

@ -173,6 +173,8 @@ const int SQL_SERVER_MAX_FIELD_SIZE = 8000;
const int SQL_SERVER_MAX_PRECISION = 38;
const int SQL_SERVER_MAX_TYPE_SIZE = 0;
const int SQL_SERVER_MAX_PARAMS = 2100;
const int SQL_SERVER_MAX_MONEY_SCALE = 4;
// increase the maximum message length to accommodate for the long error returned for operand type clash
// or for conversion of a long string
const int SQL_MAX_ERROR_MESSAGE_LENGTH = SQL_MAX_MESSAGE_LENGTH * 2;
@ -230,6 +232,9 @@ enum SQLSRV_FETCH_TYPE {
// buffer size of a sql state (including the null character)
const int SQL_SQLSTATE_BUFSIZE = SQL_SQLSTATE_SIZE + 1;
// default value of decimal places (no formatting required)
const short NO_CHANGE_DECIMAL_PLACES = -1;
// buffer size allocated to retrieve data from a PHP stream. This number
// was chosen since PHP doesn't return more than 8k at a time even if
// the amount requested was more.
@ -1108,6 +1113,7 @@ enum SQLSRV_STMT_OPTIONS {
SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE,
SQLSRV_STMT_OPTION_DATE_AS_STRING,
SQLSRV_STMT_OPTION_FORMAT_DECIMALS,
SQLSRV_STMT_OPTION_DECIMAL_PLACES,
// Driver specific connection options
SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000,
@ -1302,6 +1308,11 @@ 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 );
};
struct stmt_option_decimal_places : 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 {
@ -1372,7 +1383,7 @@ struct sqlsrv_output_param {
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
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 ) :
@ -1399,15 +1410,9 @@ struct sqlsrv_output_param {
meta_data.nullable = nullable;
}
SQLSMALLINT getDecimalDigits()
param_meta_data& getMetaData()
{
// 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;
}
return meta_data;
}
};
@ -1435,7 +1440,8 @@ 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)
bool format_decimals; // false by default but the user can set this to true to add the missing leading zeroes and/or control number of decimal digits to show
short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits)
// 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
@ -1476,9 +1482,10 @@ struct field_meta_data {
SQLULEN field_precision;
SQLSMALLINT field_scale;
SQLSMALLINT field_is_nullable;
bool field_is_money_type;
field_meta_data() : field_name_len(0), field_type(0), field_size(0), field_precision(0),
field_scale (0), field_is_nullable(0)
field_scale (0), field_is_nullable(0), field_is_money_type(false)
{
}
@ -1527,7 +1534,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);
void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC);
//*********************************************************************************************************************************
// Result Set
@ -1769,8 +1776,7 @@ 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,
SQLSRV_ERROR_INVALID_DECIMAL_PLACES,
// Driver specific error codes starts from here.
SQLSRV_ERROR_DRIVER_SPECIFIC = 1000,

View file

@ -107,7 +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 format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _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 );
@ -142,7 +142,8 @@ 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
format_decimals(false), // no formatting needed
decimal_places(NO_CHANGE_DECIMAL_PLACES), // the default is no formatting to resultset 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
send_streams_at_exec( true ),
@ -925,6 +926,19 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL
}
}
if (meta_data->field_type == SQL_DECIMAL) {
// Check if it is money type -- get the name of the data type
char field_type_name[SS_MAXCOLNAMELEN] = {'\0'};
SQLSMALLINT out_buff_len;
SQLLEN not_used;
core::SQLColAttribute(stmt, colno + 1, SQL_DESC_TYPE_NAME, field_type_name,
sizeof( field_type_name ), &out_buff_len, &not_used TSRMLS_CC);
if (!strcmp(field_type_name, "money") || !strcmp(field_type_name, "smallmoney")) {
meta_data->field_is_money_type = true;
}
}
// Set the field name lenth
meta_data->field_name_len = static_cast<SQLSMALLINT>( field_name_len );
@ -1258,20 +1272,21 @@ 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)
void core_sqlsrv_set_decimal_places(_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) {
CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_DECIMAL_PLACES) {
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();
zend_long decimal_places = Z_LVAL_P(value_z);
if (decimal_places < 0 || decimal_places > SQL_SERVER_MAX_MONEY_SCALE) {
// ignore decimal_places because it is out of range
decimal_places = NO_CHANGE_DECIMAL_PLACES;
}
stmt->num_decimals = static_cast<short>(format_decimals);
stmt->decimal_places = static_cast<short>(decimal_places);
}
catch( core::CoreException& ) {
throw;
@ -1447,7 +1462,17 @@ 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 )
{
core_sqlsrv_set_format_decimals(stmt, value_z TSRMLS_CC);
if (zend_is_true(value_z)) {
stmt->format_decimals = true;
}
else {
stmt->format_decimals = false;
}
}
void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC )
{
core_sqlsrv_set_decimal_places(stmt, value_z TSRMLS_CC);
}
// internal function to release the active stream. Called by each main API function
@ -2114,25 +2139,29 @@ void field_cache_dtor( _Inout_ zval* data_z )
}
// 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)
void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _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: decimals_places is NO_CHANGE_DECIMAL_PLACES by default, which means no formatting on decimal data is necessary
// This function assumes stmt->format_decimals is true, so it first checks if it is necessary to add the leading zero.
//
// 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
// Likewise, if decimals_places is larger than the field scale, decimals_places wil be ignored. 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 data 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
// format_decimals and decimals_places
//
std::string str = field_value;
size_t pos = str.find_first_of('.');
if (pos == std::string::npos || decimals_digits < 0) {
// The decimal dot is not found, simply return
if (pos == std::string::npos) {
return;
}
SQLSMALLINT num_decimals = decimals_digits;
SQLSMALLINT num_decimals = decimals_places;
if (num_decimals > field_scale) {
num_decimals = field_scale;
}
@ -2160,47 +2189,24 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT f
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()) {
if (num_decimals == NO_CHANGE_DECIMAL_PLACES) {
// Add the minus sign back if negative
if (isNegative) {
std::ostringstream oss;
oss << '-' << str.substr(0);
str = oss.str();
}
} else {
// Start formatting
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 pos2 all the way to the first digit
// 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 = pos2 - 1; p >= 0 && carry_over; p--) {
if (str[p] == '.') { // Skip the dot
continue;
}
for (short p = pos - 1; p >= 0 && carry_over; p--) {
n = str[p] - '0';
if (n == 9) {
str[p] = '0' ;
@ -2214,22 +2220,54 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT f
}
if (carry_over) {
std::ostringstream oss;
oss << '1' << str.substr(0, pos2);
oss << '1' << str.substr(0, pos);
str = oss.str();
pos2++;
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);
}
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();
@ -2313,7 +2351,7 @@ 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();
param_meta_data metaData = output_param->getMetaData();
if (output_param->encoding != SQLSRV_ENCODING_CHAR) {
char* outString = NULL;
@ -2323,15 +2361,16 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
throw core::CoreException();
}
if (stmt->num_decimals >= 0 && decimal_digits >= 0) {
format_decimal_numbers(stmt->num_decimals, decimal_digits, outString, &outLen);
if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) {
format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.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);
if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) {
format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len);
}
core::sqlsrv_zval_stringl(value_z, str, str_len);
@ -2601,8 +2640,10 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
}
}
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);
if (stmt->format_decimals && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) {
// number of decimal places only affect money / smallmoney fields
SQLSMALLINT decimal_places = (stmt->current_meta_data[field_index]->field_is_money_type) ? stmt->decimal_places : NO_CHANGE_DECIMAL_PLACES;
format_decimal_numbers(decimal_places, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp);
}
} // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE )

View file

@ -46,6 +46,45 @@ struct date_as_string_func {
}
};
struct format_decimals_func
{
static void func(connection_option const* /*option*/, _In_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/ TSRMLS_DC)
{
TSRMLS_C; // show as used to avoid a warning
ss_sqlsrv_conn* ss_conn = static_cast<ss_sqlsrv_conn*>(conn);
if (zend_is_true(value)) {
ss_conn->format_decimals = true;
}
else {
ss_conn->format_decimals = false;
}
}
};
struct decimal_places_func
{
static void func(connection_option const* /*option*/, _In_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/ TSRMLS_DC)
{
TSRMLS_C; // show as used to avoid a warning
// first check if the input is an integer
if (Z_TYPE_P(value) != IS_LONG) {
THROW_SS_ERROR(conn, SQLSRV_ERROR_INVALID_DECIMAL_PLACES);
}
zend_long decimal_places = Z_LVAL_P(value);
if (decimal_places < 0 || decimal_places > SQL_SERVER_MAX_MONEY_SCALE) {
decimal_places = NO_CHANGE_DECIMAL_PLACES;
}
ss_sqlsrv_conn* ss_conn = static_cast<ss_sqlsrv_conn*>(conn);
ss_conn->decimal_places = static_cast<short>(decimal_places);
}
};
struct conn_char_set_func {
static void func( connection_option const* /*option*/, _Inout_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/ TSRMLS_DC )
@ -175,6 +214,7 @@ namespace SSStmtOptionNames {
const char CLIENT_BUFFER_MAX_SIZE[] = INI_BUFFERED_QUERY_LIMIT;
const char DATE_AS_STRING[] = "ReturnDatesAsStrings";
const char FORMAT_DECIMALS[] = "FormatDecimals";
const char DECIMAL_PLACES[] = "DecimalPlaces";
}
namespace SSConnOptionNames {
@ -192,6 +232,8 @@ const char ConnectionPooling[] = "ConnectionPooling";
const char ConnectRetryCount[] = "ConnectRetryCount";
const char ConnectRetryInterval[] = "ConnectRetryInterval";
const char Database[] = "Database";
const char DecimalPlaces[] = "DecimalPlaces";
const char FormatDecimals[] = "FormatDecimals";
const char DateAsString[] = "ReturnDatesAsStrings";
const char Driver[] = "Driver";
const char Encrypt[] = "Encrypt";
@ -217,6 +259,8 @@ const char WSID[] = "WSID";
enum SS_CONN_OPTIONS {
SS_CONN_OPTION_DATE_AS_STRING = SQLSRV_CONN_OPTION_DRIVER_SPECIFIC,
SS_CONN_OPTION_FORMAT_DECIMALS,
SS_CONN_OPTION_DECIMAL_PLACES,
};
//List of all statement options supported by this driver
@ -257,6 +301,12 @@ const stmt_option SS_STMT_OPTS[] = {
SQLSRV_STMT_OPTION_FORMAT_DECIMALS,
std::unique_ptr<stmt_option_format_decimals>( new stmt_option_format_decimals )
},
{
SSStmtOptionNames::DECIMAL_PLACES,
sizeof( SSStmtOptionNames::DECIMAL_PLACES),
SQLSRV_STMT_OPTION_DECIMAL_PLACES,
std::unique_ptr<stmt_option_decimal_places>( new stmt_option_decimal_places )
},
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
};
@ -515,6 +565,24 @@ const connection_option SS_CONN_OPTS[] = {
CONN_ATTR_BOOL,
date_as_string_func::func
},
{
SSConnOptionNames::FormatDecimals,
sizeof( SSConnOptionNames::FormatDecimals),
SS_CONN_OPTION_FORMAT_DECIMALS,
SSConnOptionNames::FormatDecimals,
sizeof( SSConnOptionNames::FormatDecimals),
CONN_ATTR_BOOL,
format_decimals_func::func
},
{
SSConnOptionNames::DecimalPlaces,
sizeof( SSConnOptionNames::DecimalPlaces),
SS_CONN_OPTION_DECIMAL_PLACES,
SSConnOptionNames::DecimalPlaces,
sizeof( SSConnOptionNames::DecimalPlaces),
CONN_ATTR_INT,
decimal_places_func::func
},
{ NULL, 0, SQLSRV_CONN_OPTION_INVALID, NULL, 0 , CONN_ATTR_INVALID, NULL }, //terminate the table
};

View file

@ -131,6 +131,8 @@ struct ss_sqlsrv_conn : sqlsrv_conn
{
HashTable* stmts;
bool date_as_string;
bool format_decimals; // flag set to turn on formatting for values of decimal / numeric types
short decimal_places; // number of decimal digits to show in a result set unless format_numbers is false
bool in_transaction; // flag set when inside a transaction and used for checking validity of tran API calls
// static variables used in process_params
@ -142,6 +144,8 @@ struct ss_sqlsrv_conn : sqlsrv_conn
sqlsrv_conn( h, e, drv, SQLSRV_ENCODING_SYSTEM TSRMLS_CC ),
stmts( NULL ),
date_as_string( false ),
format_decimals( false ),
decimal_places( NO_CHANGE_DECIMAL_PLACES ),
in_transaction( false )
{
}

View file

@ -139,9 +139,11 @@ ss_sqlsrv_stmt::ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_
{
core_sqlsrv_set_buffered_query_limit( this, SQLSRV_G( buffered_query_limit ) TSRMLS_CC );
// initialize date_as_string based on the corresponding connection option
// inherit other values based on the corresponding connection options
ss_sqlsrv_conn* ss_conn = static_cast<ss_sqlsrv_conn*>(conn);
date_as_string = ss_conn->date_as_string;
format_decimals = ss_conn->format_decimals;
decimal_places = ss_conn->decimal_places;
}
ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void )

View file

@ -429,13 +429,9 @@ ss_error SS_ERRORS[] = {
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false}
},
{
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
SQLSRV_ERROR_INVALID_DECIMAL_PLACES,
{ 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, {} }

View file

@ -1,24 +1,17 @@
--TEST--
Test statement attribute PDO::SQLSRV_ATTR_FORMAT_DECIMALS for decimal types
Test connection and statement attributes for formatting decimal and numeric data (feature request issue 415)
--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.
Test the attributes PDO::SQLSRV_ATTR_FORMAT_DECIMALS and PDO::SQLSRV_ATTR_DECIMAL_PLACES, the latter affects money types only, not decimal or numeric types (feature request issue 415).
Money, decimal or numeric types 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.
Setting PDO::SQLSRV_ATTR_FORMAT_DECIMALS to false will turn off all formatting, regardless of PDO::SQLSRV_ATTR_DECIMAL_PLACES value. Also, any negative PDO::SQLSRV_ATTR_DECIMAL_PLACES value will be ignored. Likewise, since money or smallmoney fields have scale 4, if PDO::SQLSRV_ATTR_DECIMAL_PLACES value is larger than 4, it will be ignored as well.
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
2. Set PDO::SQLSRV_ATTR_FORMAT_DECIMALS to true to add the leading zeroes to money and decimal types, if missing.
3. No support for output params
The attributes PDO::SQLSRV_ATTR_FORMAT_DECIMALS and PDO::SQLSRV_ATTR_DECIMAL_PLACES will only format the
fetched results and have no effect on other operations like insertion or update.
--ENV--
PHPT_EXEC=true
--SKIPIF--
@ -35,171 +28,52 @@ function checkException($exception, $expected)
}
}
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)
{
$expected = 'Expected an integer to specify number of decimals to format the output values of decimal data types';
$query = "SELECT 0.0001";
try {
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => 0.9);
$stmt = $conn->prepare($query, $options);
$conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, 0);
$format = $conn->getAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS);
if ($format !== false) {
echo 'The value of PDO::SQLSRV_ATTR_FORMAT_DECIMALS should be false\n';
var_dump($format);
}
$conn->setAttribute(PDO::SQLSRV_ATTR_DECIMAL_PLACES, 1.5);
} 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);
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => 0.9);
$stmt = $conn->prepare($query, $options);
} catch (PdoException $e) {
checkException($e, $expected);
}
try {
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => true);
$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)
function compareNumbers($actual, $input, $column, $fieldScale, $format = true)
{
$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 {
$expected = number_format($input, $fieldScale);
// if no formatting, there will be no leading zero
$expected = number_format($input, $fieldScale);
if (!$format) {
if (abs($input) < 1) {
// Since no formatting, the leading zero should not be there
trace("Drop leading zero of $input--");
trace("Drop leading zero of $input: ");
$expected = str_replace('0.', '.', $expected);
}
}
@ -207,7 +81,7 @@ function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal =
if ($actual === $expected) {
$matched = true;
} else {
echo "For $column ($formatDecimal): expected $expected ($input) but the value is $actual\n";
echo "For $column ($fieldScale): expected $expected ($input) but the value is $actual\n";
}
}
return $matched;
@ -223,35 +97,35 @@ function testNoOption($conn, $tableName, $inputs, $columns)
$results = $stmt->fetch(PDO::FETCH_NUM);
trace("\ntestNoOption:\n");
for ($i = 0; $i < count($inputs); $i++) {
compareNumbers($results[$i], $inputs[$i], $columns[$i], $i);
compareNumbers($results[$i], $inputs[$i], $columns[$i], $i, false);
}
}
function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $withBuffer)
function testStmtOption($conn, $tableName, $inputs, $columns, $decimalPlaces, $withBuffer)
{
// Decimal values should return decimal digits based on the valid statement
// option PDO::SQLSRV_ATTR_FORMAT_DECIMALS
// Decimal values should NOT be affected by the statement
// attribute 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);
PDO::SQLSRV_ATTR_DECIMAL_PLACES => $decimalPlaces);
} else {
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $formatDecimal);
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => $decimalPlaces);
}
$size = count($inputs);
$stmt = $conn->prepare($query, $options);
// Fetch by getting one field at a time
trace("\ntestStmtOption: $formatDecimal and buffered $withBuffer\n");
trace("\ntestStmtOption: $decimalPlaces 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);
compareNumbers($field, $inputs[$i], $columns[$i], $i);
}
}
@ -262,7 +136,7 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $ino
$outSql = getCallProcSqlPlaceholders($storedProcName, 1);
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits);
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => $numDigits);
$stmt = $conn->prepare($outSql, $options);
$len = 1024;
@ -279,17 +153,17 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $ino
$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
// unaffected by the statement attr PDO::SQLSRV_ATTR_DECIMAL_PLACES.
// If ColumnEncryption is enabled, in which case the driver is able
// to derive the decimal type, leading zero will be added if missing
if (isAEConnected()) {
trace("\ngetOutputParam ($inout) with AE:\n");
$column = 'outputParamAE';
compareNumbers($outString, $inputValue, $column, $scale, $numDigits);
compareNumbers($outString, $inputValue, $column, $scale, true);
} else {
trace("\ngetOutputParam ($inout) without AE:\n");
$column = 'outputParam';
compareNumbers($outString, $inputValue, $column, $scale);
compareNumbers($outString, $inputValue, $column, $scale, false);
}
}
@ -314,16 +188,8 @@ try {
$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';
@ -347,8 +213,8 @@ try {
$n = rand(1, 6);
$neg = ($n % 2 == 0) ? -1 : 1;
// $n1 may or may not be negative
$n1 = rand(0, 1000) * $neg;
// $n1, a tiny number, which may or may not be negative,
$n1 = rand(0, 5) * $neg;
if ($s > 0) {
$max *= 10;
@ -371,6 +237,11 @@ try {
testNoOption($conn, $tableName, $values, $columns, true);
// Turn on formatting, which only add leading zeroes, if missing
// decimal and numeric types should be unaffected by
// PDO::SQLSRV_ATTR_DECIMAL_PLACES whatsoever
$conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, 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);

View file

@ -1,248 +0,0 @@
--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--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");
$prec = 38;
$dot = '.';
function createTestTable($conn)
{
global $prec;
// Create the test table of one decimal column
$tableName = "pdoFormatDecimalScales";
$colMeta = array();
$max = $prec + 1;
for ($i = 0; $i < $max; $i++) {
$scale = $prec - $i;
$column = "col_$scale";
$dataType = "decimal($prec, $scale)";
array_push($colMeta, new ColumnMeta($dataType, $column));
}
createTable($conn, $tableName, $colMeta);
return $tableName;
}
function insertTestData($conn, $tableName)
{
global $prec, $dot;
$temp = str_repeat('1234567890', 4);
$digits = substr($temp, 0, $prec + 1);
$inputData = array();
$max = $prec + 1;
// Generate input strings - replace the $i-th digit with a dot '.'
for ($i = 0; $i < $max; $i++) {
$d = $digits[$i];
$digits[$i] = $dot;
if ($i == $prec) {
$digits = substr($temp, 0, $prec);
}
$scale = $prec - $i;
$column = "col_$scale";
$inputData = array_merge($inputData, array($column => $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

View file

@ -0,0 +1,162 @@
--TEST--
Test various decimal places of money values (feature request issue 415)
--DESCRIPTION--
In SQL Server, the maximum precision of money type is 19 with scale 4. Generate a long numeric string and get rid of the last digit to make it a 15-digit-string. Then replace one digit at a time with a dot '.' to make it a decimal input string for testing.
For example,
string(15) ".23456789098765"
string(15) "1.3456789098765"
string(15) "12.456789098765"
string(15) "123.56789098765"
string(15) "1234.6789098765"
...
string(15) "1234567890987.5"
string(15) "12345678909876."
The inserted money data will be
0.2346
1.3457
12.4568
123.5679
1234.6789
...
1234567890987.5000
12345678909876.0000
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");
$prec = 19;
$scale = 4;
$dot = '.';
function createTestTable($conn)
{
global $prec, $scale;
// Create the test table
$tableName = "pdoFormatMoneyScales";
$colMeta = array();
$max = $prec - $scale;
for ($i = 0; $i < $max; $i++) {
$column = "col_$i";
$dataType = 'money';
array_push($colMeta, new ColumnMeta($dataType, $column));
}
createTable($conn, $tableName, $colMeta);
return $tableName;
}
function insertTestData($conn, $tableName)
{
global $prec, $scale, $dot;
$digits = substr('1234567890987654321', 0, $prec - $scale);
$inputData = array();
$max = $prec - $scale;
// Generate input strings - replace the $i-th digit with a dot '.'
for ($i = 0; $i < $max; $i++) {
$d = $digits[$i];
$digits[$i] = $dot;
$column = "col_$i";
$inputData = array_merge($inputData, array($column => $digits));
// Restore the $i-th digit with its original digit
$digits[$i] = $d;
}
$stmt = insertRow($conn, $tableName, $inputData);
unset($stmt);
}
function numberFormat($value, $numDecimals)
{
return number_format($value, $numDecimals, '.', '');
}
/****
The function testVariousScales() will fetch one column at a time, using scale from 0 up to 4 allowed for that column type.
For example, if the input string is
1234567890.2345
When fetching data, using scale from 0 to 4, the following values are expected to return:
1234567890
1234567890.2
1234567890.23
1234567890.235
1234567890.2345
****/
function testVariousScales($conn, $tableName)
{
global $prec, $scale;
$max = $prec - $scale;
for ($i = 0; $i < $max; $i++) {
$column = "col_$i";
$query = "SELECT $column as col1 FROM $tableName";
// Default case: no formatting
$stmt = $conn->query($query);
if ($obj = $stmt->fetchObject()) {
trace("\n$obj->col1\n");
$input = $obj->col1;
} else {
echo "In testVariousScales: fetchObject failed\n";
}
// Next, format how many decimals to be displayed
$query = "SELECT $column FROM $tableName";
for ($j = 0; $j <= $scale; $j++) {
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => true, PDO::SQLSRV_ATTR_DECIMAL_PLACES => $j);
$stmt = $conn->prepare($query, $options);
$stmt->execute();
$stmt->bindColumn($column, $value);
if ($stmt->fetch(PDO::FETCH_BOUND)) {
trace("$value\n");
$expected = numberFormat($input, $j);
if ($value !== $expected) {
echo "testVariousScales ($j): Expected $expected but got $value\n";
}
} else {
echo "Round $i scale $j: fetch failed\n";
}
}
}
}
try {
// This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION
// Default is no formatting, but set it to false anyway
$conn = connect();
$conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, false);
$tableName = createTestTable($conn);
insertTestData($conn, $tableName);
testVariousScales($conn, $tableName);
dropTable($conn, $tableName);
echo "Done\n";
unset($conn);
} catch (PdoException $e) {
echo $e->getMessage() . PHP_EOL;
}
?>
--EXPECT--
Done

View file

@ -0,0 +1,244 @@
--TEST--
Test connection attributes for formatting money data (feature request issue 415)
--DESCRIPTION--
Test how money data in the fetched values can be formatted by using the connection attributes PDO::SQLSRV_ATTR_FORMAT_DECIMALS and PDO::SQLSRV_ATTR_DECIMAL_PLACES, the latter works only with integer values. No effect on other operations like insertion or update.
The PDO::SQLSRV_ATTR_DECIMAL_PLACES attribute only affects money/smallmoney fields. If its value is out of range, for example, it's negative or larger than the original scale, then its value will be ignored.
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). For this reason, it is not recommended to use formatted money values as inputs to any calculation.
The corresponding statement attributes always override the inherited values from the connection object. Setting PDO::SQLSRV_ATTR_FORMAT_DECIMALS to false will automatically turn off any formatting of decimal data in the result set, ignoring PDO::SQLSRV_ATTR_DECIMAL_PLACES value.
By only setting PDO::SQLSRV_ATTR_FORMAT_DECIMALS to true will add the leading zeroes, if missing.
Do not support output params.
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");
function numberFormat($value, $numDecimals)
{
return number_format($value, $numDecimals, '.', '');
}
function testFloatTypes($conn, $numDigits)
{
// 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,
// because the connection attributes for formatting have no effect
$epsilon = 0.001;
$nColumns = 5;
$values = array();
for ($i = 0; $i < $nColumns; $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);
// By default the floating point numbers are fetched as strings
$stmt = $conn->prepare($query);
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 "$diff: Expected $floatVal but $floatVal1 returned. \n";
}
}
}
}
function verifyMoneyFormatting($conn, $query, $values, $format)
{
if ($format) {
// Set SQLSRV_ATTR_FORMAT_DECIMALS to true but
// set SQLSRV_ATTR_DECIMAL_PLACES to a negative number
// to override the inherited attribute
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => -1, PDO::SQLSRV_ATTR_FORMAT_DECIMALS => true);
} else {
// Set SQLSRV_ATTR_FORMAT_DECIMALS to false will
// turn off any formatting -- overriding the inherited
// attributes
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => false);
}
$stmt = $conn->prepare($query, $options);
$stmt->execute();
$results = $stmt->fetch(PDO::FETCH_NUM);
trace("\verifyMoneyFormatting:\n");
for ($i = 0; $i < count($values); $i++) {
// money types have a scale of 4
$default = numberFormat($values[$i], 4);
if (!$format) {
// No formatting - should drop the leading zero, if exists
if (abs($values[$i]) < 1) {
$default = str_replace('0.', '.', $default);
}
}
if ($default !== $results[$i]) {
echo "verifyMoneyFormatting ($format): Expected $default but got $results[$i]\n";
}
}
}
function verifyMoneyValues($conn, $numDigits, $query, $values, $override)
{
if ($override) {
$options = array(PDO::SQLSRV_ATTR_DECIMAL_PLACES => $numDigits);
$stmt = $conn->prepare($query, $options);
} else {
// Use the connection defaults
$stmt = $conn->prepare($query);
}
$stmt->execute();
$results = $stmt->fetch(PDO::FETCH_NUM);
trace("\nverifyMoneyValues:\n");
for ($i = 0; $i < count($values); $i++) {
$value = numberFormat($values[$i], $numDigits);
trace("$results[$i], $value\n");
if ($value !== $results[$i]) {
echo "testMoneyTypes ($override, $numDigits): Expected $value but got $results[$i]\n";
}
}
}
function testMoneyTypes($conn, $numDigits)
{
// With money and smallmoney types, which are essentially decimal types
// As of today, ODBC driver does not support Always Encrypted feature with money / smallmoney
$values = array();
$nColumns = 6;
for ($i = 0; $i < $nColumns; $i++) {
// First get a random number
$n = rand(0, 10);
$neg = ($n % 2 == 0) ? -1 : 1;
// $n1 may or may not be negative
$max = 10;
$n1 = rand(0, $max) * $neg;
$n2 = rand(1, $max * 1000);
$number = sprintf("%d.%d", $n1, $n2);
array_push($values, $number);
}
$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])";
// Do not override the connection attributes
verifyMoneyValues($conn, $numDigits, $query, $values, false);
// Next, override statement attribute to set number of
// decimal places
verifyMoneyValues($conn, 0, $query, $values, true);
// Set Formatting attribute to true then false
verifyMoneyFormatting($conn, $query, $values, true);
verifyMoneyFormatting($conn, $query, $values, false);
}
function connGetAttributes($conn, $numDigits)
{
$format = $conn->getAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS);
if ($format !== true) {
echo "The returned value of SQLSRV_ATTR_FORMAT_DECIMALS, $format, is wrong\n";
return false;
}
$digits = $conn->getAttribute(PDO::SQLSRV_ATTR_DECIMAL_PLACES);
if ($digits != $numDigits) {
echo "The returned value of SQLSRV_ATTR_DECIMAL_PLACES, $digits, is wrong\n";
return false;
}
return true;
}
function connectWithAttrs($numDigits)
{
$attr = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => true,
PDO::SQLSRV_ATTR_DECIMAL_PLACES => $numDigits);
// This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION
$conn = connect('', $attr);
if (connGetAttributes($conn, $numDigits)) {
// First test with money types
testMoneyTypes($conn, $numDigits);
// Also test using regular floats
testFloatTypes($conn, $numDigits);
}
unset($conn);
}
function connectSetAttrs($numDigits)
{
// This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION
$conn = connect();
$conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, true);
$conn->setAttribute(PDO::SQLSRV_ATTR_DECIMAL_PLACES, $numDigits);
if (connGetAttributes($conn, $numDigits)) {
// First test with money types
testMoneyTypes($conn, $numDigits);
// Also test using regular floats
testFloatTypes($conn, $numDigits);
}
unset($conn);
}
try {
connectWithAttrs(2);
connectSetAttrs(3);
echo "Done\n";
unset($conn);
} catch (PdoException $e) {
echo $e->getMessage() . PHP_EOL;
}
?>
--EXPECT--
Done

View file

@ -1,22 +1,18 @@
--TEST--
Test how decimal data output values can be formatted (feature request issue 415)
Test connection and statement attributes for formatting decimal and numeric data (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.
Test the connection and statement options, FormatDecimals and
DecimalPlaces, the latter affects money types only, not
decimal or numeric types (feature request issue 415).
Money, decimal or numeric types 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.
No effect on other operations like insertion or update.
Setting FormatDecimals to false will turn off all formatting, regardless of DecimalPlaces value. Also, any negative DecimalPlaces value will be ignored. Likewise, since money or smallmoney fields have scale 4, if DecimalPlaces value is larger than 4, it will be ignored as well.
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
1. By default, data will be returned with the original precision and scale
2. Set FormatDecimals to true to add the leading zeroes to money and decimal types, if missing.
3. For output params, leading zeroes will be added for any decimal fields if FormatDecimals is true, but only if either SQLSRV_SQLTYPE_DECIMAL or SQLSRV_SQLTYPE_NUMERIC is set correctly to match the original column type and its precision / scale.
FormatDecimals and DecimalPlaces will only format the fetched results and have no effect on other operations like insertion or update.
--ENV--
PHPT_EXEC=true
--SKIPIF--
@ -25,23 +21,19 @@ PHPT_EXEC=true
<?php
require_once('MsCommon.inc');
function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal = -1)
function compareNumbers($actual, $input, $column, $fieldScale, $format = true)
{
$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 {
$expected = number_format($input, $fieldScale);
// If no formatting, there will be no leading zero
$expected = number_format($input, $fieldScale);
if (!$format) {
if (abs($input) < 1) {
// Since no formatting, the leading zero should not be there
trace("Drop leading zero of $input--");
trace("Drop leading zero of $input: ");
$expected = str_replace('0.', '.', $expected);
}
}
@ -49,7 +41,7 @@ function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal =
if ($actual === $expected) {
$matched = true;
} else {
echo "For $column ($formatDecimal): expected $expected ($input) but the value is $actual\n";
echo "For $column ($fieldScale): expected $expected ($input) but the value is $actual\n";
}
}
return $matched;
@ -58,134 +50,34 @@ function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal =
function testErrorCases($conn)
{
$query = "SELECT 0.0001";
$options = array('FormatDecimals' => 1.5);
$message = 'Expected an integer to specify number of decimals to format the output values of decimal data types.';
$options = array('DecimalPlaces' => 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);
$options = array('DecimalPlaces' => true);
$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));
$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 && $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 {
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
// This should return decimal values as they are
$query = "SELECT * FROM $tableName";
if ($exec) {
$stmt = sqlsrv_query($conn, $query);
@ -194,27 +86,27 @@ function testNoOption($conn, $tableName, $inputs, $columns, $exec)
sqlsrv_execute($stmt);
}
// Compare values
// 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);
compareNumbers($results[$i], $inputs[$i], $columns[$i], $i, false);
}
}
function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $withBuffer)
function testStmtOption($conn, $tableName, $inputs, $columns, $decimalPlaces, $withBuffer)
{
// Decimal values should return decimal digits based on the valid statement
// option FormatDecimals
// Decimal values should NOT be affected by the statement
// option DecimalPlaces
$query = "SELECT * FROM $tableName";
if ($withBuffer){
$options = array('Scrollable' => 'buffered', 'FormatDecimals' => $formatDecimal);
$options = array('Scrollable' => 'buffered', 'DecimalPlaces' => $decimalPlaces);
} else {
$options = array('FormatDecimals' => $formatDecimal);
$options = array('DecimalPlaces' => $decimalPlaces);
}
$size = count($inputs);
$stmt = sqlsrv_prepare($conn, $query, array(), $options);
// Fetch by getting one field at a time
sqlsrv_execute($stmt);
@ -223,26 +115,28 @@ function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $w
}
for ($i = 0; $i < $size; $i++) {
$field = sqlsrv_get_field($stmt, $i); // Expect a string
compareNumbers($field, $inputs[$i], $columns[$i], $i, $formatDecimal);
compareNumbers($field, $inputs[$i], $columns[$i], $i, true);
}
}
function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $inout)
function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $numeric, $inout)
{
$outString = '';
$numDigits = 2;
$dir = SQLSRV_PARAM_OUT;
// 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
// The output param value should be the same as the input,
// unaffected by the statement attr DecimalPlaces. If
// the correct sql type is specified or ColumnEncryption
// is enabled, in which case the driver is able to derive
// the correct field type, leading zero will be added
// if missing
$sqlType = null;
if (!AE\isColEncrypted()) {
$sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale);
$type = ($numeric) ? 'SQLSRV_SQLTYPE_NUMERIC' : 'SQLSRV_SQLTYPE_DECIMAL';
$sqlType = call_user_func($type, $prec, $scale);
}
// For inout parameters the input type should match the output one
if ($inout) {
$dir = SQLSRV_PARAM_INOUT;
@ -250,38 +144,37 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $ino
}
$outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1);
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outString, $dir, null, $sqlType)),
array('FormatDecimals' => $numDigits));
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outString, $dir, null, $sqlType)),
array('DecimalPlaces' => $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
// Verify value of output param
$column = 'outputParam';
compareNumbers($outString, $inputValue, $column, $scale, $numDigits);
compareNumbers($outString, $inputValue, $column, $scale, true);
sqlsrv_free_stmt($stmt);
if (!AE\isColEncrypted()) {
// With ColumnEncryption enabled, the driver is able to derive the decimal type,
// so skip this part of the test
$outString2 = $inout ? '0.0' : '';
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outString2, $dir)),
array('FormatDecimals' => $numDigits));
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outString2, $dir)),
array('DecimalPlaces' => $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);
compareNumbers($outString2, $inputValue, $column, $scale, true);
sqlsrv_free_stmt($stmt);
}
}
@ -296,8 +189,8 @@ function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inou
createProc($conn, $storedProcName, $procArgs, $procCode);
// Call stored procedure to retrieve output param
getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $inout);
getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $i > 2, $inout);
dropProc($conn, $storedProcName);
}
}
@ -310,16 +203,10 @@ if (!$conn) {
fatalError("Could not connect.\n");
}
// First to test if leading zero is added
testMoneyTypes($conn);
// Then test error conditions
// Test error conditions
testErrorCases($conn);
// Also test using regular floats
testFloatTypes($conn);
// Create the test table of decimal / numeric data columns
// Create the test table of decimal / numeric data columns
$tableName = 'sqlsrvFormatDecimals';
$columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6');
@ -338,13 +225,13 @@ $values = array();
$max2 = 1;
for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) {
// First get a random number
$n = rand(0, 10);
$n = rand(1, 6);
$neg = ($n % 2 == 0) ? -1 : 1;
// $n1 may or may not be negative
$max1 = 1000;
// $n1 is a tiny number, which may or may not be negative
$max1 = 5;
$n1 = rand(0, $max1) * $neg;
if ($s > 0) {
$max2 *= 10;
$n2 = rand(0, $max2);
@ -352,7 +239,7 @@ for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) {
} else {
$number = sprintf("%d", $n1);
}
array_push($values, $number);
}
@ -373,6 +260,14 @@ sqlsrv_free_stmt($stmt);
testNoOption($conn, $tableName, $values, $columns, true);
testNoOption($conn, $tableName, $values, $columns, false);
sqlsrv_close($conn);
// Reconnect with FormatDecimals option set to true
$conn = AE\connect(array('FormatDecimals' => true));
if (!$conn) {
fatalError("Could not connect.\n");
}
// Now try with setting number decimals to 3 then 2
testStmtOption($conn, $tableName, $values, $columns, 3, false);
testStmtOption($conn, $tableName, $values, $columns, 3, true);
@ -384,7 +279,7 @@ testStmtOption($conn, $tableName, $values, $columns, 2, true);
testOutputParam($conn, $tableName, $values, $columns, $dataTypes);
testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true);
dropTable($conn, $tableName);
dropTable($conn, $tableName);
sqlsrv_close($conn);
echo "Done\n";

View file

@ -1,255 +0,0 @@
--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--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
$prec = 38;
$dot = '.';
function createTestTable($conn)
{
global $prec;
// Create the test table of one decimal column
$tableName = "sqlsrvFormatDecimalScales";
$colMeta = array();
$max = $prec + 1;
for ($i = 0; $i < $max; $i++) {
$scale = $prec - $i;
$column = "col_$scale";
$dataType = "decimal($prec, $scale)";
array_push($colMeta, new AE\ColumnMeta($dataType, $column));
}
AE\createTable($conn, $tableName, $colMeta);
return $tableName;
}
function insertTestData($conn, $tableName)
{
global $prec, $dot;
$temp = str_repeat('1234567890', 4);
$digits = substr($temp, 0, $prec + 1);
$inputData = array();
$max = $prec + 1;
// Generate input strings - replace the $i-th digit with a dot '.'
for ($i = 0; $i < $max; $i++) {
$d = $digits[$i];
$digits[$i] = $dot;
if ($i == $prec) {
$digits = substr($temp, 0, $prec);
}
$scale = $prec - $i;
$column = "col_$scale";
$inputData = array_merge($inputData, array($column => $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

View file

@ -0,0 +1,167 @@
--TEST--
Test various decimal places of money values (feature request issue 415)
--DESCRIPTION--
In SQL Server, the maximum precision of money type is 19 with scale 4. Generate a long numeric string and get rid of the last digit to make it a 15-digit-string. Then replace one digit at a time with a dot '.' to make it a decimal input string for testing.
For example,
string(15) ".23456789098765"
string(15) "1.3456789098765"
string(15) "12.456789098765"
string(15) "123.56789098765"
string(15) "1234.6789098765"
...
string(15) "1234567890987.5"
string(15) "12345678909876."
The inserted money data will be
0.2346
1.3457
12.4568
123.5679
1234.6789
...
1234567890987.5000
12345678909876.0000
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
$prec = 19;
$scale = 4;
$dot = '.';
function createTestTable($conn)
{
global $prec, $scale;
// Create the test table
$tableName = "sqlsrvFormatMoneyScales";
$colMeta = array();
$max = $prec - $scale;
for ($i = 0; $i < $max; $i++) {
$column = "col_$i";
$dataType = 'money';
array_push($colMeta, new AE\ColumnMeta($dataType, $column));
}
AE\createTable($conn, $tableName, $colMeta);
return $tableName;
}
function insertTestData($conn, $tableName)
{
global $prec, $scale, $dot;
$digits = substr('1234567890987654321', 0, $prec - $scale);
$inputData = array();
$max = $prec - $scale;
// Generate input strings - replace the $i-th digit with a dot '.'
for ($i = 0; $i < $max; $i++) {
$d = $digits[$i];
$digits[$i] = $dot;
$column = "col_$i";
$inputData = array_merge($inputData, array($column => $digits));
trace($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);
}
function numberFormat($value, $numDecimals)
{
return number_format($value, $numDecimals, '.', '');
}
/****
The function testVariousScales() will fetch one column at a time, using scale from 0 up to 4 allowed for that column type.
For example, if the input string is
1234567890.2345
When fetching data, using scale from 0 to 4, the following values are expected to return:
1234567890
1234567890.2
1234567890.23
1234567890.235
1234567890.2345
****/
function testVariousScales($conn, $tableName)
{
global $prec, $scale;
$max = $prec - $scale;
for ($i = 0; $i < $max; $i++) {
$column = "col_$i";
$query = "SELECT $column as col1 FROM $tableName";
// Default case: no formatting
$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");
$input = $obj->col1;
} else {
fatalError("In testVariousScales: sqlsrv_fetch_object failed\n");
}
// Next, format how many decimals to be displayed
$query = "SELECT $column FROM $tableName";
for ($j = 0; $j <= $scale; $j++) {
$options = array('FormatDecimals' => true,'DecimalPlaces' => $j);
$stmt = sqlsrv_query($conn, $query, array(), $options);
if (sqlsrv_fetch($stmt)) {
$value = sqlsrv_get_field($stmt, 0);
trace("$value\n");
$expected = numberFormat($input, $j);
if ($value !== $expected) {
echo "testVariousScales ($j): Expected $expected but got $value\n";
}
} else {
fatalError("Round $i scale $j: sqlsrv_fetch failed\n");
}
}
}
}
set_time_limit(0);
sqlsrv_configure('WarningsReturnAsErrors', 1);
// Default is no formatting, but set it to false anyway
$conn = AE\connect(array('FormatDecimals' => false));
if (!$conn) {
fatalError("Could not connect.\n");
}
$tableName = createTestTable($conn);
insertTestData($conn, $tableName);
testVariousScales($conn, $tableName);
dropTable($conn, $tableName);
sqlsrv_close($conn);
echo "Done\n";
?>
--EXPECT--
Done

View file

@ -0,0 +1,261 @@
--TEST--
Test the options for formatting money data (feature request issue 415)
--DESCRIPTION--
Test how money data in the fetched values can be formatted by using the connection
option FormatDecimals and DecimalPlaces, the latter works only with integer
values. No effect on other operations like insertion or update.
The option DecimalPlaces only affects money/smallmoney fields. If its value is out of range, for example, it's negative or larger than the original scale, then its value will be ignored.
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). For this reason, it is not recommended to use formatted money values as inputs to any calculation.
The corresponding statement options always override the inherited values from the connection object. Setting FormatDecimals to false will automatically turn off any formatting of decimal data in the result set, ignoring DecimalPlaces value.
By only setting FormatDecimals to true will add the leading zeroes, if missing. For output params, missing zeroes will be added if either SQLSRV_SQLTYPE_MONEY or SQLSRV_SQLTYPE_SMALLMONEY is set as the SQLSRV SQL Type.
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
function numberFormat($value, $numDecimals)
{
return number_format($value, $numDecimals, '.', '');
}
function testFloatTypes($conn)
{
global $numDigits; // inherited from connection option
// 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,
// because connection options for formatting have no effect
$epsilon = 0.001;
$nColumns = 5;
$values = array();
for ($i = 0; $i < $nColumns; $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 = sqlsrv_query($conn, $query);
$floats = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
if (!$floats) {
echo "testFloatTypes: sqlsrv_fetch_array failed\n";
}
// The number of decimals in each of the results will vary
$stmt = sqlsrv_query($conn, $query);
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 && $floatVal != $floats[$i]) {
echo "Expected $floats[$i] but returned ";
var_dump($floatVal);
} else {
$diff = abs($floatVal - $floats[$i]) / $floats[$i];
if ($diff > $epsilon) {
echo "$diff: Expected $floats[$i] but returned ";
var_dump($floatVal);
}
}
}
} else {
echo "testFloatTypes: sqlsrv_fetch failed\n";
}
}
function verifyMoneyValues($conn, $numDigits, $query, $values, $override)
{
if ($override) {
$options = array('DecimalPlaces' => $numDigits);
$stmt = sqlsrv_query($conn, $query, array(), $options);
} else {
$stmt = sqlsrv_query($conn, $query);
}
$results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
trace("\nverifyMoneyValues:\n");
for ($i = 0; $i < count($values); $i++) {
$value = numberFormat($values[$i], $numDigits);
trace("$results[$i], $value\n");
if ($value !== $results[$i]) {
echo "verifyMoneyValues ($override, $numDigits): Expected $value but got $results[$i]\n";
}
}
}
function verifyMoneyFormatting($conn, $query, $values, $format)
{
if ($format) {
// Set FormatDecimals to true to turn on formatting, but setting
// DecimalPlaces to a negative number, which will be ignored.
$nDigits = -1;
$options = array('FormatDecimals' => true, 'DecimalPlaces' => $nDigits);
$stmt = sqlsrv_query($conn, $query, array(), $options);
} else {
// Set FormatDecimals to false to turn off formatting.
// This should override the inherited connection
// options, and by default, money and smallmoney types
// have scale of 4 digits
$options = array('FormatDecimals' => false);
$stmt = sqlsrv_query($conn, $query, array(), $options);
}
$results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
for ($i = 0; $i < count($values); $i++) {
$default = numberFormat($values[$i], 4);
if (!$format) {
// No formatting - should drop the leading zero, if exists
if (abs($values[$i]) < 1) {
$default = str_replace('0.', '.', $default);
}
}
if ($default !== $results[$i]) {
echo "verifyMoneyFormatting ($format): Expected default $default but got $results[$i]\n";
}
}
}
function getOutputParam($conn, $spProcName, $input, $money, $inout)
{
$outString = '0.0';
$dir = ($inout) ? SQLSRV_PARAM_INOUT : SQLSRV_PARAM_OUT;
$sqlType = ($money) ? SQLSRV_SQLTYPE_MONEY : SQLSRV_SQLTYPE_SMALLMONEY;
$outSql = AE\getCallProcSqlPlaceholders($spProcName, 1);
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outString, $dir, null, $sqlType)));
if (!$stmt) {
fatalError("getOutputParam: failed when preparing to call $spProcName");
}
if (!sqlsrv_execute($stmt)) {
fatalError("getOutputParam: failed to execute procedure $spProcName");
}
// FormatDecimals only add leading zeroes, but do
// not support controlling decimal places, so
// use scale 4 for money/smallmoney types
$expected = numberFormat($input, 4);
trace("getOutputParam result is $outString and expected $expected\n");
if ($outString !== $expected) {
echo "getOutputParam ($inout): Expected $expected but got $outString\n";
var_dump($expected);
var_dump($outString);
}
}
function testOutputParam($conn)
{
// Create a table for testing output param
$tableName = 'sqlsrvMoneyFormats';
$values = array(0.12345, 0.34567);
$query = "SELECT CONVERT(smallmoney, $values[0]) AS m1,
CONVERT(money, $values[1]) AS m2
INTO $tableName";
$stmt = sqlsrv_query($conn, $query);
for ($i = 0; $i < 2; $i++) {
// Create the stored procedure first
$storedProcName = "spMoneyFormats" . $i;
$dataType = ($i == 0) ? 'smallmoney' : 'money';
$procArgs = "@col $dataType OUTPUT";
$column = 'm' . ($i + 1);
$procCode = "SELECT @col = $column FROM $tableName";
createProc($conn, $storedProcName, $procArgs, $procCode);
getOutputParam($conn, $storedProcName, $values[$i], $i, false);
getOutputParam($conn, $storedProcName, $values[$i], $i, true);
dropProc($conn, $storedProcName);
}
dropTable($conn, $tableName);
}
function testMoneyTypes($conn)
{
global $numDigits; // inherited from connection option
// With money and smallmoney types, which are essentially decimal types
// As of today, ODBC driver does not support Always Encrypted feature with money / smallmoney
$values = array();
$nColumns = 6;
for ($i = 0; $i < $nColumns; $i++) {
// First get a random number
$n = rand(0, 10);
$neg = ($n % 2 == 0) ? -1 : 1;
// $n1 may or may not be negative
$max = 10;
$n1 = rand(0, $max) * $neg;
$n2 = rand(1, $max * 1000);
$number = sprintf("%d.%d", $n1, $n2);
array_push($values, $number);
}
$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])";
// Do not override the connection attributes
verifyMoneyValues($conn, $numDigits, $query, $values, false);
// Next, override statement attribute to set number of
// decimal places to 0
verifyMoneyValues($conn, 0, $query, $values, true);
// Set Formatting attribute to true then false
verifyMoneyFormatting($conn, $query, $values, true);
verifyMoneyFormatting($conn, $query, $values, false);
}
set_time_limit(0);
sqlsrv_configure('WarningsReturnAsErrors', 1);
$numDigits = 2;
$conn = AE\connect(array('FormatDecimals' => true, 'DecimalPlaces' => $numDigits));
if (!$conn) {
fatalError("Could not connect.\n");
}
// First to test if leading zero is added
testMoneyTypes($conn);
// Also test using regular floats
testFloatTypes($conn);
// Test output params
testOutputParam($conn);
sqlsrv_close($conn);
echo "Done\n";
?>
--EXPECT--
Done