Feature request 415 for pdo_sqlsrv (#873)
This commit is contained in:
parent
2a9398f7e0
commit
f4ad2ae1d4
|
@ -80,7 +80,8 @@ enum PDO_STMT_OPTIONS {
|
||||||
PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE,
|
PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE,
|
||||||
PDO_STMT_OPTION_EMULATE_PREPARES,
|
PDO_STMT_OPTION_EMULATE_PREPARES,
|
||||||
PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE,
|
PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE,
|
||||||
PDO_STMT_OPTION_FETCHES_DATETIME_TYPE
|
PDO_STMT_OPTION_FETCHES_DATETIME_TYPE,
|
||||||
|
PDO_STMT_OPTION_FORMAT_DECIMALS
|
||||||
};
|
};
|
||||||
|
|
||||||
// List of all the statement options supported by this driver.
|
// List of all the statement options supported by this driver.
|
||||||
|
@ -95,6 +96,7 @@ const stmt_option PDO_STMT_OPTS[] = {
|
||||||
{ NULL, 0, PDO_STMT_OPTION_EMULATE_PREPARES, std::unique_ptr<stmt_option_emulate_prepares>( new stmt_option_emulate_prepares ) },
|
{ NULL, 0, PDO_STMT_OPTION_EMULATE_PREPARES, std::unique_ptr<stmt_option_emulate_prepares>( new stmt_option_emulate_prepares ) },
|
||||||
{ 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_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_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, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
|
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
|
||||||
};
|
};
|
||||||
|
@ -1095,6 +1097,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
|
||||||
case PDO_ATTR_EMULATE_PREPARES:
|
case PDO_ATTR_EMULATE_PREPARES:
|
||||||
case PDO_ATTR_CURSOR:
|
case PDO_ATTR_CURSOR:
|
||||||
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
|
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
|
||||||
|
case SQLSRV_ATTR_FORMAT_DECIMALS:
|
||||||
{
|
{
|
||||||
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR );
|
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR );
|
||||||
}
|
}
|
||||||
|
@ -1153,6 +1156,7 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
|
||||||
case PDO_ATTR_EMULATE_PREPARES:
|
case PDO_ATTR_EMULATE_PREPARES:
|
||||||
case PDO_ATTR_CURSOR:
|
case PDO_ATTR_CURSOR:
|
||||||
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
|
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
|
||||||
|
case SQLSRV_ATTR_FORMAT_DECIMALS:
|
||||||
{
|
{
|
||||||
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR );
|
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR );
|
||||||
}
|
}
|
||||||
|
@ -1586,6 +1590,10 @@ void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_
|
||||||
option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE;
|
option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SQLSRV_ATTR_FORMAT_DECIMALS:
|
||||||
|
option_key = PDO_STMT_OPTION_FORMAT_DECIMALS;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) {
|
CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) {
|
||||||
throw core::CoreException();
|
throw core::CoreException();
|
||||||
|
|
|
@ -286,6 +286,7 @@ namespace {
|
||||||
{ "SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE", SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE },
|
{ "SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE", SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE },
|
||||||
{ "SQLSRV_ATTR_FETCHES_NUMERIC_TYPE", SQLSRV_ATTR_FETCHES_NUMERIC_TYPE },
|
{ "SQLSRV_ATTR_FETCHES_NUMERIC_TYPE", SQLSRV_ATTR_FETCHES_NUMERIC_TYPE },
|
||||||
{ "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE },
|
{ "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE },
|
||||||
|
{ "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS },
|
||||||
|
|
||||||
// used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int,
|
// 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
|
// PDO::PARAM_STR uses the size of the string in the variable
|
||||||
|
|
|
@ -882,6 +882,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In
|
||||||
driver_stmt->fetch_datetime = ( zend_is_true( val )) ? true : false;
|
driver_stmt->fetch_datetime = ( zend_is_true( val )) ? true : false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SQLSRV_ATTR_FORMAT_DECIMALS:
|
||||||
|
core_sqlsrv_set_format_decimals(driver_stmt, val TSRMLS_CC);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR );
|
THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR );
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -437,6 +437,14 @@ pdo_error PDO_ERRORS[] = {
|
||||||
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
|
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
|
||||||
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false}
|
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
|
||||||
|
{ IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -92, false}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE,
|
||||||
|
{ IMSSP, (SQLCHAR*) "For formatting decimal data values, %1!d! is out of range. Expected an integer from 0 to 38, inclusive.", -93, true}
|
||||||
|
},
|
||||||
|
|
||||||
{ UINT_MAX, {} }
|
{ UINT_MAX, {} }
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,14 +41,15 @@ extern "C" {
|
||||||
// sqlsrv driver specific PDO attributes
|
// sqlsrv driver specific PDO attributes
|
||||||
enum PDO_SQLSRV_ATTR {
|
enum PDO_SQLSRV_ATTR {
|
||||||
|
|
||||||
// Currently there are only three custom attributes for this driver.
|
// The custom attributes for this driver:
|
||||||
SQLSRV_ATTR_ENCODING = PDO_ATTR_DRIVER_SPECIFIC,
|
SQLSRV_ATTR_ENCODING = PDO_ATTR_DRIVER_SPECIFIC,
|
||||||
SQLSRV_ATTR_QUERY_TIMEOUT,
|
SQLSRV_ATTR_QUERY_TIMEOUT,
|
||||||
SQLSRV_ATTR_DIRECT_QUERY,
|
SQLSRV_ATTR_DIRECT_QUERY,
|
||||||
SQLSRV_ATTR_CURSOR_SCROLL_TYPE,
|
SQLSRV_ATTR_CURSOR_SCROLL_TYPE,
|
||||||
SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE,
|
SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE,
|
||||||
SQLSRV_ATTR_FETCHES_NUMERIC_TYPE,
|
SQLSRV_ATTR_FETCHES_NUMERIC_TYPE,
|
||||||
SQLSRV_ATTR_FETCHES_DATETIME_TYPE
|
SQLSRV_ATTR_FETCHES_DATETIME_TYPE,
|
||||||
|
SQLSRV_ATTR_FORMAT_DECIMALS
|
||||||
};
|
};
|
||||||
|
|
||||||
// valid set of values for TransactionIsolation connection option
|
// valid set of values for TransactionIsolation connection option
|
||||||
|
|
|
@ -1527,7 +1527,7 @@ void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z
|
||||||
bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );
|
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_ 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_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit TSRMLS_DC );
|
||||||
|
void core_sqlsrv_set_format_decimals(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC);
|
||||||
|
|
||||||
//*********************************************************************************************************************************
|
//*********************************************************************************************************************************
|
||||||
// Result Set
|
// Result Set
|
||||||
|
@ -1707,7 +1707,6 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set {
|
||||||
|
|
||||||
// utility functions shared by multiple callers across files
|
// utility functions shared by multiple callers across files
|
||||||
bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_updates_z_(len) char** string, _Inout_ SQLLEN& len);
|
bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_updates_z_(len) char** string, _Inout_ SQLLEN& len);
|
||||||
bool convert_zval_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _Inout_ zval* value_z, _Inout_ SQLLEN& len);
|
|
||||||
bool validate_string( _In_ char* string, _In_ SQLLEN& len);
|
bool validate_string( _In_ char* string, _In_ SQLLEN& len);
|
||||||
bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(cchInLen) const SQLWCHAR* inString, _In_ SQLINTEGER cchInLen, _Inout_updates_bytes_(cchOutLen) char** outString, _Out_ SQLLEN& cchOutLen );
|
bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(cchInLen) const SQLWCHAR* inString, _In_ SQLINTEGER cchInLen, _Inout_updates_bytes_(cchOutLen) char** outString, _Out_ SQLLEN& cchOutLen );
|
||||||
SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len );
|
SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len );
|
||||||
|
|
|
@ -1258,6 +1258,26 @@ void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void core_sqlsrv_set_format_decimals(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// first check if the input is an integer
|
||||||
|
CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) {
|
||||||
|
throw core::CoreException();
|
||||||
|
}
|
||||||
|
|
||||||
|
zend_long format_decimals = Z_LVAL_P(value_z);
|
||||||
|
CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) {
|
||||||
|
throw core::CoreException();
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt->num_decimals = static_cast<short>(format_decimals);
|
||||||
|
}
|
||||||
|
catch( core::CoreException& ) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC )
|
void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC )
|
||||||
{
|
{
|
||||||
TSRMLS_C;
|
TSRMLS_C;
|
||||||
|
@ -1427,17 +1447,7 @@ void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op
|
||||||
|
|
||||||
void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC )
|
void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC )
|
||||||
{
|
{
|
||||||
// first check if the input is an integer
|
core_sqlsrv_set_format_decimals(stmt, value_z TSRMLS_CC);
|
||||||
CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) {
|
|
||||||
throw core::CoreException();
|
|
||||||
}
|
|
||||||
|
|
||||||
zend_long format_decimals = Z_LVAL_P(value_z);
|
|
||||||
CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) {
|
|
||||||
throw core::CoreException();
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt->num_decimals = static_cast<short>(format_decimals);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal function to release the active stream. Called by each main API function
|
// internal function to release the active stream. Called by each main API function
|
||||||
|
@ -2293,27 +2303,39 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
|
||||||
str_len = output_param->original_buffer_len - null_size;
|
str_len = output_param->original_buffer_len - null_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's not in the 8 bit encodings, then it's in UTF-16
|
if (output_param->encoding == SQLSRV_ENCODING_BINARY) {
|
||||||
if( output_param->encoding != SQLSRV_ENCODING_CHAR && output_param->encoding != SQLSRV_ENCODING_BINARY ) {
|
|
||||||
bool converted = convert_zval_string_from_utf16(output_param->encoding, value_z, str_len);
|
|
||||||
CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) {
|
|
||||||
throw core::CoreException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( output_param->encoding == SQLSRV_ENCODING_BINARY && str_len < output_param->original_buffer_len ) {
|
|
||||||
// ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated
|
// ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated
|
||||||
// so we do that here if the length of the returned data is less than the original allocation. The
|
// so we do that here if the length of the returned data is less than the original allocation. The
|
||||||
// original allocation null terminates the buffer already.
|
// original allocation null terminates the buffer already.
|
||||||
str[str_len] = '\0';
|
if (str_len < output_param->original_buffer_len) {
|
||||||
|
str[str_len] = '\0';
|
||||||
|
}
|
||||||
core::sqlsrv_zval_stringl(value_z, str, str_len);
|
core::sqlsrv_zval_stringl(value_z, str, str_len);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SQLSMALLINT decimal_digits = output_param->getDecimalDigits();
|
SQLSMALLINT decimal_digits = output_param->getDecimalDigits();
|
||||||
if (stmt->num_decimals >= 0 && decimal_digits >= 0) {
|
|
||||||
format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
core::sqlsrv_zval_stringl(value_z, str, str_len);
|
if (output_param->encoding != SQLSRV_ENCODING_CHAR) {
|
||||||
|
char* outString = NULL;
|
||||||
|
SQLLEN outLen = 0;
|
||||||
|
bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast<const SQLWCHAR*>(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen );
|
||||||
|
CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) {
|
||||||
|
throw core::CoreException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stmt->num_decimals >= 0 && decimal_digits >= 0) {
|
||||||
|
format_decimal_numbers(stmt->num_decimals, decimal_digits, outString, &outLen);
|
||||||
|
}
|
||||||
|
core::sqlsrv_zval_stringl(value_z, outString, outLen);
|
||||||
|
sqlsrv_free(outString);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (stmt->num_decimals >= 0 && decimal_digits >= 0) {
|
||||||
|
format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
core::sqlsrv_zval_stringl(value_z, str, str_len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -91,25 +91,6 @@ bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_up
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool convert_zval_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _Inout_ zval* value_z, _Inout_ SQLLEN& len)
|
|
||||||
{
|
|
||||||
char* string = Z_STRVAL_P(value_z);
|
|
||||||
|
|
||||||
if( validate_string(string, len)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* outString = NULL;
|
|
||||||
SQLLEN outLen = 0;
|
|
||||||
bool result = convert_string_from_utf16( encoding, reinterpret_cast<const SQLWCHAR*>(string), int(len / sizeof(SQLWCHAR)), &outString, outLen );
|
|
||||||
if( result ) {
|
|
||||||
core::sqlsrv_zval_stringl( value_z, outString, outLen );
|
|
||||||
sqlsrv_free( outString );
|
|
||||||
len = outLen;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool validate_string( _In_ char* string, _In_ SQLLEN& len )
|
bool validate_string( _In_ char* string, _In_ SQLLEN& len )
|
||||||
{
|
{
|
||||||
SQLSRV_ASSERT(string != NULL, "String must be specified");
|
SQLSRV_ASSERT(string != NULL, "String must be specified");
|
||||||
|
|
390
test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt
Normal file
390
test/functional/pdo_sqlsrv/pdostatement_format_decimals.phpt
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
--TEST--
|
||||||
|
Test statement attribute PDO::SQLSRV_ATTR_FORMAT_DECIMALS for decimal types
|
||||||
|
--DESCRIPTION--
|
||||||
|
Test statement attribute PDO::SQLSRV_ATTR_FORMAT_DECIMALS for decimal or
|
||||||
|
money types (feature request issue 415), which are always fetched as strings
|
||||||
|
to preserve accuracy and precision, unlike other primitive numeric types,
|
||||||
|
where there is an option to retrieve them as numbers.
|
||||||
|
|
||||||
|
This attribute expects an integer value from the range [0,38], the money or
|
||||||
|
decimal types in the fetched result set can be formatted.
|
||||||
|
|
||||||
|
No effect on other operations like insertion or update.
|
||||||
|
|
||||||
|
1. By default, data will be returned with the original precision and scale
|
||||||
|
2. The data column original scale still takes precedence – for example, if the user
|
||||||
|
specifies 3 decimal digits for a column of decimal(5,2), the result still shows only 2
|
||||||
|
decimals to the right of the dot
|
||||||
|
3. After formatting, the missing leading zeroes will be padded
|
||||||
|
4. The underlying data will not be altered, but formatted results may likely be rounded
|
||||||
|
up (e.g. .2954 will be displayed as 0.30 if the user wants only two decimals)
|
||||||
|
5. Do not support output params
|
||||||
|
--ENV--
|
||||||
|
PHPT_EXEC=true
|
||||||
|
--SKIPIF--
|
||||||
|
<?php require('skipif_mid-refactor.inc'); ?>
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
require_once("MsCommon_mid-refactor.inc");
|
||||||
|
|
||||||
|
function checkException($exception, $expected)
|
||||||
|
{
|
||||||
|
if (strpos($exception->getMessage(), $expected) === false) {
|
||||||
|
print_r($exception->getMessage());
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPdoAttribute($conn, $setAttr)
|
||||||
|
{
|
||||||
|
// Expects exception because PDO::SQLSRV_ATTR_FORMAT_DECIMALS
|
||||||
|
// is a statement level attribute
|
||||||
|
try {
|
||||||
|
$res = true;
|
||||||
|
if ($setAttr) {
|
||||||
|
$res = $conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, 1);
|
||||||
|
} else {
|
||||||
|
$res = $conn->getAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS);
|
||||||
|
}
|
||||||
|
if ($res) {
|
||||||
|
echo "setAttribute at PDO level should have failed!\n";
|
||||||
|
}
|
||||||
|
} catch (PdoException $e) {
|
||||||
|
if ($setAttr) {
|
||||||
|
$expected = 'The given attribute is only supported on the PDOStatement object.';
|
||||||
|
} else {
|
||||||
|
$expected = 'driver does not support that attribute';
|
||||||
|
}
|
||||||
|
|
||||||
|
checkException($e, $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testErrorCases($conn)
|
||||||
|
{
|
||||||
|
$query = "SELECT 0.0001";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => 0.9);
|
||||||
|
$stmt = $conn->prepare($query, $options);
|
||||||
|
} catch (PdoException $e) {
|
||||||
|
$expected = 'Expected an integer to specify number of decimals to format the output values of decimal data types';
|
||||||
|
checkException($e, $expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => 100);
|
||||||
|
$stmt = $conn->prepare($query, $options);
|
||||||
|
} catch (PdoException $e) {
|
||||||
|
$expected = 'For formatting decimal data values, 100 is out of range. Expected an integer from 0 to 38, inclusive.';
|
||||||
|
checkException($e, $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyMoneyValues($conn, $query, $values, $numDigits)
|
||||||
|
{
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits);
|
||||||
|
$stmt = $conn->prepare($query, $options);
|
||||||
|
$stmt->execute();
|
||||||
|
$results = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
|
|
||||||
|
trace("\nverifyMoneyValues:\n");
|
||||||
|
for ($i = 0; $i < count($values); $i++) {
|
||||||
|
$value = number_format($values[$i], $numDigits);
|
||||||
|
trace("$results[$i], $value\n");
|
||||||
|
|
||||||
|
if ($value !== $results[$i]) {
|
||||||
|
echo "testMoneyTypes: Expected $value but got $results[$i]\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testFloatTypes($conn)
|
||||||
|
{
|
||||||
|
// This test with the float types of various number of bits, which are retrieved
|
||||||
|
// as numbers by default. When fetched as strings, no formatting is done even with
|
||||||
|
// the statement option FormatDecimals set
|
||||||
|
$epsilon = 0.001;
|
||||||
|
$values = array();
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$n1 = rand(1, 100);
|
||||||
|
$n2 = rand(1, 100);
|
||||||
|
$neg = ($i % 2 == 0) ? -1 : 1;
|
||||||
|
|
||||||
|
$n = $neg * $n1 / $n2;
|
||||||
|
array_push($values, $n);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "SELECT CONVERT(float(1), $values[0]),
|
||||||
|
CONVERT(float(12), $values[1]),
|
||||||
|
CONVERT(float(24), $values[2]),
|
||||||
|
CONVERT(float(36), $values[3]),
|
||||||
|
CONVERT(float(53), $values[4])";
|
||||||
|
$stmt = $conn->query($query);
|
||||||
|
$floats = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
|
unset($stmt);
|
||||||
|
|
||||||
|
// Set PDO::SQLSRV_ATTR_FORMAT_DECIMALS to 2 should
|
||||||
|
// have no effect on floating point numbers
|
||||||
|
$numDigits = 2;
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits);
|
||||||
|
$stmt = $conn->prepare($query, $options);
|
||||||
|
|
||||||
|
// By default the floating point numbers are fetched as strings
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$stmt->execute();
|
||||||
|
$floatStr = $stmt->fetchColumn($i);
|
||||||
|
|
||||||
|
$floatVal = floatVal($floats[$i]);
|
||||||
|
$floatVal1 = floatval($floatStr);
|
||||||
|
|
||||||
|
trace("testFloatTypes: $floatVal1, $floatVal\n");
|
||||||
|
|
||||||
|
// Check if the numbers of decimal digits are the same
|
||||||
|
// It is highly unlikely but not impossible
|
||||||
|
$numbers = explode('.', $floatStr);
|
||||||
|
$len = strlen($numbers[1]);
|
||||||
|
if ($len == $numDigits && $floatVal1 != $floatVal) {
|
||||||
|
echo "Expected $floatVal but $floatVal1 returned. \n";
|
||||||
|
} else {
|
||||||
|
$diff = abs($floatVal1 - $floatVal) / $floatVal;
|
||||||
|
if ($diff > $epsilon) {
|
||||||
|
echo "Expected $floatVal but $floatVal1 returned. \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMoneyTypes($conn)
|
||||||
|
{
|
||||||
|
// With money and smallmoney types, which are essentially decimal types
|
||||||
|
// ODBC driver does not support Always Encrypted feature with money / smallmoney
|
||||||
|
$values = array('24.559', '0', '-0.946', '0.2985', '-99.675', '79.995');
|
||||||
|
$defaults = array('24.5590', '.0000', '-.9460', '.2985', '-99.6750', '79.9950');
|
||||||
|
|
||||||
|
$query = "SELECT CONVERT(smallmoney, $values[0]),
|
||||||
|
CONVERT(money, $values[1]),
|
||||||
|
CONVERT(smallmoney, $values[2]),
|
||||||
|
CONVERT(money, $values[3]),
|
||||||
|
CONVERT(smallmoney, $values[4]),
|
||||||
|
CONVERT(money, $values[5])";
|
||||||
|
|
||||||
|
$stmt = $conn->query($query);
|
||||||
|
$results = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
|
for ($i = 0; $i < count($values); $i++) {
|
||||||
|
if ($defaults[$i] !== $results[$i]) {
|
||||||
|
echo "testMoneyTypes: Expected $defaults[$i] but got $results[$i]\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($stmt);
|
||||||
|
|
||||||
|
// Set PDO::SQLSRV_ATTR_FORMAT_DECIMALS to 0 then 2
|
||||||
|
verifyMoneyValues($conn, $query, $values, 0);
|
||||||
|
verifyMoneyValues($conn, $query, $values, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNumbers($actual, $input, $column, $fieldScale, $formatDecimal = -1)
|
||||||
|
{
|
||||||
|
$matched = false;
|
||||||
|
if ($actual === $input) {
|
||||||
|
$matched = true;
|
||||||
|
trace("$actual, $input\n");
|
||||||
|
} else {
|
||||||
|
// When $formatDecimal is negative, that means no formatting done
|
||||||
|
// Otherwise, if $formatDecimal > $fieldScale, will show $fieldScale decimal digits
|
||||||
|
if ($formatDecimal >= 0) {
|
||||||
|
$numDecimals = ($formatDecimal > $fieldScale) ? $fieldScale : $formatDecimal;
|
||||||
|
} else {
|
||||||
|
$numDecimals = $fieldScale;
|
||||||
|
}
|
||||||
|
$expected = number_format($input, $numDecimals);
|
||||||
|
trace("$actual, $expected\n");
|
||||||
|
if ($actual === $expected) {
|
||||||
|
$matched = true;
|
||||||
|
} else {
|
||||||
|
echo "For $column: expected $expected but the value is $actual\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNoOption($conn, $tableName, $inputs, $columns)
|
||||||
|
{
|
||||||
|
// Without the statement option, should return decimal values as they are
|
||||||
|
$query = "SELECT * FROM $tableName";
|
||||||
|
$stmt = $conn->query($query);
|
||||||
|
|
||||||
|
// Compare values
|
||||||
|
$results = $stmt->fetch(PDO::FETCH_NUM);
|
||||||
|
trace("\ntestNoOption:\n");
|
||||||
|
for ($i = 0; $i < count($inputs); $i++) {
|
||||||
|
compareNumbers($results[$i], $inputs[$i], $columns[$i], $i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $withBuffer)
|
||||||
|
{
|
||||||
|
// Decimal values should return decimal digits based on the valid statement
|
||||||
|
// option PDO::SQLSRV_ATTR_FORMAT_DECIMALS
|
||||||
|
$query = "SELECT * FROM $tableName";
|
||||||
|
if ($withBuffer){
|
||||||
|
$options = array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
|
||||||
|
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
|
||||||
|
PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $formatDecimal);
|
||||||
|
} else {
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $formatDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = count($inputs);
|
||||||
|
$stmt = $conn->prepare($query, $options);
|
||||||
|
|
||||||
|
// Fetch by getting one field at a time
|
||||||
|
trace("\ntestStmtOption: $formatDecimal and buffered $withBuffer\n");
|
||||||
|
for ($i = 0; $i < $size; $i++) {
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$stmt->bindColumn($columns[$i], $field);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_BOUND);
|
||||||
|
|
||||||
|
compareNumbers($field, $inputs[$i], $columns[$i], $i, $formatDecimal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $inout)
|
||||||
|
{
|
||||||
|
$outString = '';
|
||||||
|
$numDigits = 2;
|
||||||
|
|
||||||
|
$outSql = getCallProcSqlPlaceholders($storedProcName, 1);
|
||||||
|
|
||||||
|
$options = array(PDO::SQLSRV_ATTR_FORMAT_DECIMALS => $numDigits);
|
||||||
|
$stmt = $conn->prepare($outSql, $options);
|
||||||
|
|
||||||
|
$len = 1024;
|
||||||
|
if ($inout) {
|
||||||
|
$paramType = PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT;
|
||||||
|
|
||||||
|
// For inout parameters the input type should match the output one
|
||||||
|
$outString = '0.0';
|
||||||
|
} else {
|
||||||
|
$paramType = PDO::PARAM_STR;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->bindParam(1, $outString, $paramType, $len);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// The output param value should be the same as the input value,
|
||||||
|
// unaffected by the statement attr PDO::SQLSRV_ATTR_FORMAT_DECIMALS,
|
||||||
|
// unless ColumnEncryption is enabled, in which case the driver is able
|
||||||
|
// to derive the decimal type
|
||||||
|
if (isAEConnected()) {
|
||||||
|
trace("\ngetOutputParam ($inout) with AE:\n");
|
||||||
|
$column = 'outputParamAE';
|
||||||
|
compareNumbers($outString, $inputValue, $column, $scale, $numDigits);
|
||||||
|
} else {
|
||||||
|
trace("\ngetOutputParam ($inout) without AE:\n");
|
||||||
|
$column = 'outputParam';
|
||||||
|
compareNumbers($outString, $inputValue, $column, $scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inout = false)
|
||||||
|
{
|
||||||
|
for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) {
|
||||||
|
// Create the stored procedure first
|
||||||
|
$storedProcName = "spFormatDecimals" . $i;
|
||||||
|
$procArgs = "@col $dataTypes[$i] OUTPUT";
|
||||||
|
$procCode = "SELECT @col = $columns[$i] FROM $tableName";
|
||||||
|
createProc($conn, $storedProcName, $procArgs, $procCode);
|
||||||
|
|
||||||
|
// Call stored procedure to retrieve output param
|
||||||
|
getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $inout);
|
||||||
|
|
||||||
|
dropProc($conn, $storedProcName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION
|
||||||
|
$conn = connect();
|
||||||
|
|
||||||
|
// Test some error conditions
|
||||||
|
testPdoAttribute($conn, true);
|
||||||
|
testPdoAttribute($conn, false);
|
||||||
|
testErrorCases($conn);
|
||||||
|
|
||||||
|
// First test with money types
|
||||||
|
testMoneyTypes($conn);
|
||||||
|
|
||||||
|
// Also test using regular floats
|
||||||
|
testFloatTypes($conn);
|
||||||
|
|
||||||
|
// Create the test table of decimal / numeric data columns
|
||||||
|
$tableName = 'pdoFormatDecimals';
|
||||||
|
|
||||||
|
$columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6');
|
||||||
|
$dataTypes = array('decimal(3,0)', 'decimal(4,1)', 'decimal(5,2)', 'numeric(6,3)', 'numeric(7,4)', 'numeric(8, 5)');
|
||||||
|
|
||||||
|
$colMeta = array(new ColumnMeta($dataTypes[0], $columns[0]),
|
||||||
|
new ColumnMeta($dataTypes[1], $columns[1]),
|
||||||
|
new ColumnMeta($dataTypes[2], $columns[2]),
|
||||||
|
new ColumnMeta($dataTypes[3], $columns[3]),
|
||||||
|
new ColumnMeta($dataTypes[4], $columns[4]),
|
||||||
|
new ColumnMeta($dataTypes[5], $columns[5]));
|
||||||
|
createTable($conn, $tableName, $colMeta);
|
||||||
|
|
||||||
|
// Generate random input values based on precision and scale
|
||||||
|
trace("\nGenerating random input values: \n");
|
||||||
|
$values = array();
|
||||||
|
$max = 1;
|
||||||
|
for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) {
|
||||||
|
// First get a random number
|
||||||
|
$n = rand(1, 6);
|
||||||
|
$neg = ($n % 2 == 0) ? -1 : 1;
|
||||||
|
|
||||||
|
// $n1 may or may not be negative
|
||||||
|
$n1 = rand(0, 1000) * $neg;
|
||||||
|
|
||||||
|
if ($s > 0) {
|
||||||
|
$max *= 10;
|
||||||
|
$n2 = rand(0, $max);
|
||||||
|
$number = sprintf("%d.%d", $n1, $n2);
|
||||||
|
} else {
|
||||||
|
$number = sprintf("%d", $n1);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace("$s: $number\n");
|
||||||
|
array_push($values, $number);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?)";
|
||||||
|
$stmt = $conn->prepare($query);
|
||||||
|
for ($i = 0; $i < count($columns); $i++) {
|
||||||
|
$stmt->bindParam($i+1, $values[$i]);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
testNoOption($conn, $tableName, $values, $columns, true);
|
||||||
|
|
||||||
|
// Now try with setting number decimals to 3 then 2
|
||||||
|
testStmtOption($conn, $tableName, $values, $columns, 3, false);
|
||||||
|
testStmtOption($conn, $tableName, $values, $columns, 3, true);
|
||||||
|
|
||||||
|
testStmtOption($conn, $tableName, $values, $columns, 2, false);
|
||||||
|
testStmtOption($conn, $tableName, $values, $columns, 2, true);
|
||||||
|
|
||||||
|
// Test output parameters
|
||||||
|
testOutputParam($conn, $tableName, $values, $columns, $dataTypes);
|
||||||
|
testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true);
|
||||||
|
|
||||||
|
dropTable($conn, $tableName);
|
||||||
|
echo "Done\n";
|
||||||
|
|
||||||
|
unset($stmt);
|
||||||
|
unset($conn);
|
||||||
|
} catch (PdoException $e) {
|
||||||
|
echo $e->getMessage() . PHP_EOL;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
Done
|
|
@ -0,0 +1,248 @@
|
||||||
|
--TEST--
|
||||||
|
Test various precisions of formatting decimal data output values (feature request issue 415)
|
||||||
|
--DESCRIPTION--
|
||||||
|
In SQL Server, the maximum allowed precision is 38. The scale can range from 0 up to the
|
||||||
|
defined precision. Generate a long numeric string and get rid of the last digit to make it a
|
||||||
|
39-digit-string. Then replace one digit at a time with a dot '.' to make it a decimal
|
||||||
|
input string for testing with various scales.
|
||||||
|
For example,
|
||||||
|
string(39) ".23456789012345678901234567890123456789"
|
||||||
|
string(39) "1.3456789012345678901234567890123456789"
|
||||||
|
string(39) "12.456789012345678901234567890123456789"
|
||||||
|
string(39) "123.56789012345678901234567890123456789"
|
||||||
|
string(39) "1234.6789012345678901234567890123456789"
|
||||||
|
string(39) "12345.789012345678901234567890123456789"
|
||||||
|
... ...
|
||||||
|
string(39) "1234567890123456789012345678901234.6789"
|
||||||
|
string(39) "12345678901234567890123456789012345.789"
|
||||||
|
string(39) "123456789012345678901234567890123456.89"
|
||||||
|
string(39) "1234567890123456789012345678901234567.9"
|
||||||
|
string(38) "12345678901234567890123456789012345678"
|
||||||
|
|
||||||
|
Note: PHP number_format() will not be used for verification in this test
|
||||||
|
because the function starts losing accuracy with large number of precisions / scales.
|
||||||
|
--ENV--
|
||||||
|
PHPT_EXEC=true
|
||||||
|
--SKIPIF--
|
||||||
|
<?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
|
|
@ -107,17 +107,21 @@ function testFloatTypes($conn)
|
||||||
if (sqlsrv_fetch($stmt)) {
|
if (sqlsrv_fetch($stmt)) {
|
||||||
for ($i = 0; $i < count($values); $i++) {
|
for ($i = 0; $i < count($values); $i++) {
|
||||||
$floatStr = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR));
|
$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);
|
$numbers = explode('.', $floatStr);
|
||||||
$len = strlen($numbers[1]);
|
$len = strlen($numbers[1]);
|
||||||
if ($len == $numDigits) {
|
if ($len == $numDigits && $floatVal != $floats[$i]) {
|
||||||
// This is highly unlikely
|
echo "Expected $floats[$i] but returned ";
|
||||||
var_dump($floatStr);
|
|
||||||
}
|
|
||||||
$floatVal = floatval($floatStr);
|
|
||||||
$diff = abs($floatVal - $floats[$i]) / $floats[$i];
|
|
||||||
if ($diff > $epsilon) {
|
|
||||||
var_dump($diff);
|
|
||||||
var_dump($floatVal);
|
var_dump($floatVal);
|
||||||
|
} else {
|
||||||
|
$diff = abs($floatVal - $floats[$i]) / $floats[$i];
|
||||||
|
if ($diff > $epsilon) {
|
||||||
|
echo "Expected $floats[$i] but returned ";
|
||||||
|
var_dump($floatVal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -216,17 +220,31 @@ function testStmtOption($conn, $tableName, $inputs, $columns, $formatDecimal, $w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale)
|
function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $inout)
|
||||||
{
|
{
|
||||||
$outString = '';
|
$outString = '';
|
||||||
$numDigits = 2;
|
$numDigits = 2;
|
||||||
|
$dir = SQLSRV_PARAM_OUT;
|
||||||
|
|
||||||
// Derive the sqlsrv type SQLSRV_SQLTYPE_DECIMAL($prec, $scale)
|
// The output param value should be the same as the input value,
|
||||||
$sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale);
|
// unaffected by the statement attr FormatDecimals, unless
|
||||||
|
// ColumnEncryption is enabled, in which case the driver is able
|
||||||
|
// to derive the decimal type. Another workaround is to specify
|
||||||
|
// the SQLSRV_SQLTYPE_DECIMAL type with the correct precision and scale
|
||||||
|
$sqlType = null;
|
||||||
|
if (!AE\isColEncrypted()) {
|
||||||
|
$sqlType = call_user_func('SQLSRV_SQLTYPE_DECIMAL', $prec, $scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For inout parameters the input type should match the output one
|
||||||
|
if ($inout) {
|
||||||
|
$dir = SQLSRV_PARAM_INOUT;
|
||||||
|
$outString = '0.0';
|
||||||
|
}
|
||||||
|
|
||||||
$outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1);
|
$outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1);
|
||||||
$stmt = sqlsrv_prepare($conn, $outSql,
|
$stmt = sqlsrv_prepare($conn, $outSql,
|
||||||
array(array(&$outString, SQLSRV_PARAM_OUT, null, $sqlType)),
|
array(array(&$outString, $dir, null, $sqlType)),
|
||||||
array('FormatDecimals' => $numDigits));
|
array('FormatDecimals' => $numDigits));
|
||||||
if (!$stmt) {
|
if (!$stmt) {
|
||||||
fatalError("getOutputParam: failed when preparing to call $storedProcName");
|
fatalError("getOutputParam: failed when preparing to call $storedProcName");
|
||||||
|
@ -242,14 +260,11 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale)
|
||||||
sqlsrv_free_stmt($stmt);
|
sqlsrv_free_stmt($stmt);
|
||||||
|
|
||||||
if (!AE\isColEncrypted()) {
|
if (!AE\isColEncrypted()) {
|
||||||
// Get output param without specifying sqlsrv type, and the returned value will
|
|
||||||
// be a regular string -- its value should be the same as the input value,
|
|
||||||
// unaffected by the statement option FormatDecimals
|
|
||||||
// With ColumnEncryption enabled, the driver is able to derive the decimal type,
|
// With ColumnEncryption enabled, the driver is able to derive the decimal type,
|
||||||
// so skip this part of the test
|
// so skip this part of the test
|
||||||
$outString2 = '';
|
$outString2 = $inout ? '0.0' : '';
|
||||||
$stmt = sqlsrv_prepare($conn, $outSql,
|
$stmt = sqlsrv_prepare($conn, $outSql,
|
||||||
array(array(&$outString2, SQLSRV_PARAM_OUT)),
|
array(array(&$outString2, $dir)),
|
||||||
array('FormatDecimals' => $numDigits));
|
array('FormatDecimals' => $numDigits));
|
||||||
if (!$stmt) {
|
if (!$stmt) {
|
||||||
fatalError("getOutputParam2: failed when preparing to call $storedProcName");
|
fatalError("getOutputParam2: failed when preparing to call $storedProcName");
|
||||||
|
@ -264,7 +279,7 @@ function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes)
|
function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inout = false)
|
||||||
{
|
{
|
||||||
for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) {
|
for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) {
|
||||||
// Create the stored procedure first
|
// Create the stored procedure first
|
||||||
|
@ -274,7 +289,7 @@ function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes)
|
||||||
createProc($conn, $storedProcName, $procArgs, $procCode);
|
createProc($conn, $storedProcName, $procArgs, $procCode);
|
||||||
|
|
||||||
// Call stored procedure to retrieve output param
|
// Call stored procedure to retrieve output param
|
||||||
getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i);
|
getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $inout);
|
||||||
|
|
||||||
dropProc($conn, $storedProcName);
|
dropProc($conn, $storedProcName);
|
||||||
}
|
}
|
||||||
|
@ -360,6 +375,7 @@ testStmtOption($conn, $tableName, $values, $columns, 2, true);
|
||||||
|
|
||||||
// Test output parameters
|
// Test output parameters
|
||||||
testOutputParam($conn, $tableName, $values, $columns, $dataTypes);
|
testOutputParam($conn, $tableName, $values, $columns, $dataTypes);
|
||||||
|
testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true);
|
||||||
|
|
||||||
dropTable($conn, $tableName);
|
dropTable($conn, $tableName);
|
||||||
sqlsrv_close($conn);
|
sqlsrv_close($conn);
|
||||||
|
|
Loading…
Reference in a new issue