@@ -107,6 +107,7 @@ void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_
107
107
_Out_ SQLSMALLINT& sql_type TSRMLS_DC );
108
108
void col_cache_dtor ( _Inout_ zval* data_z );
109
109
void field_cache_dtor ( _Inout_ zval* data_z );
110
+ void format_decimal_numbers (_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len);
110
111
void finalize_output_parameters ( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );
111
112
void get_field_as_string ( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type,
112
113
_Inout_updates_bytes_ (*field_len) void*& field_value, _Inout_ SQLLEN* field_len TSRMLS_DC );
@@ -141,8 +142,9 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error
141
142
past_next_result_end( false ),
142
143
query_timeout( QUERY_TIMEOUT_INVALID ),
143
144
date_as_string(false ),
145
+ num_decimals(-1 ), // -1 means no formatting required
144
146
buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ),
145
- param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte
147
+ param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte
146
148
send_streams_at_exec( true ),
147
149
current_stream( NULL , SQLSRV_ENCODING_DEFAULT ),
148
150
current_stream_read( 0 )
@@ -571,6 +573,8 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_
571
573
// save the parameter to be adjusted and/or converted after the results are processed
572
574
sqlsrv_output_param output_param ( param_ref, encoding, param_num, static_cast <SQLUINTEGER>( buffer_len ) );
573
575
576
+ output_param.saveMetaData (sql_type, column_size, decimal_digits);
577
+
574
578
save_output_param_for_later ( stmt, output_param TSRMLS_CC );
575
579
576
580
// For output parameters, if we set the column_size to be same as the buffer_len,
@@ -1416,6 +1420,21 @@ void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op
1416
1420
}
1417
1421
}
1418
1422
1423
+ void stmt_option_format_decimals:: operator ()( _Inout_ sqlsrv_stmt* stmt, stmt_option const * /* */ , _In_ zval* value_z TSRMLS_DC )
1424
+ {
1425
+ // first check if the input is an integer
1426
+ CHECK_CUSTOM_ERROR (Z_TYPE_P (value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) {
1427
+ throw core::CoreException ();
1428
+ }
1429
+
1430
+ zend_long format_decimals = Z_LVAL_P (value_z);
1431
+ CHECK_CUSTOM_ERROR (format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) {
1432
+ throw core::CoreException ();
1433
+ }
1434
+
1435
+ stmt->num_decimals = static_cast <short >(format_decimals);
1436
+ }
1437
+
1419
1438
// internal function to release the active stream. Called by each main API function
1420
1439
// that will alter the statement and cancel any retrieval of data from a stream.
1421
1440
void close_active_stream ( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
@@ -2079,6 +2098,130 @@ void field_cache_dtor( _Inout_ zval* data_z )
2079
2098
sqlsrv_free ( cache );
2080
2099
}
2081
2100
2101
+ // To be called for formatting decimal / numeric fetched values from finalize_output_parameters() and/or get_field_as_string()
2102
+ void format_decimal_numbers (_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len)
2103
+ {
2104
+ // In SQL Server, the default maximum precision of numeric and decimal data types is 38
2105
+ //
2106
+ // Note: stmt->num_decimals is -1 by default, which means no formatting on decimals / numerics is necessary
2107
+ // If the required number of decimals is larger than the field scale, will use the column field scale instead.
2108
+ // This is to ensure the number of decimals adheres to the column field scale. If smaller, the output value may be rounded up.
2109
+ //
2110
+ // Note: it's possible that the decimal / numeric value does not contain a decimal dot because the field scale is 0.
2111
+ // Thus, first check if the decimal dot exists. If not, no formatting necessary, regardless of decimals_digits
2112
+ //
2113
+ std::string str = field_value;
2114
+ size_t pos = str.find_first_of (' .' );
2115
+
2116
+ if (pos == std::string::npos || decimals_digits < 0 ) {
2117
+ return ;
2118
+ }
2119
+
2120
+ SQLSMALLINT num_decimals = decimals_digits;
2121
+ if (num_decimals > field_scale) {
2122
+ num_decimals = field_scale;
2123
+ }
2124
+
2125
+ // We want the rounding to be consistent with php number_format(), http://php.net/manual/en/function.number-format.php
2126
+ // as well as SQL Server Management studio, such that the least significant digit will be rounded up if it is
2127
+ // followed by 5 or above.
2128
+
2129
+ bool isNegative = false ;
2130
+
2131
+ // If negative, remove the minus sign for now so as not to complicate the rounding process
2132
+ if (str[0 ] == ' -' ) {
2133
+ isNegative = true ;
2134
+ std::ostringstream oss;
2135
+ oss << str.substr (1 );
2136
+ str = oss.str ();
2137
+ pos = str.find_first_of (' .' );
2138
+ }
2139
+
2140
+ // Adds the leading zero if not exists
2141
+ if (pos == 0 ) {
2142
+ std::ostringstream oss;
2143
+ oss << ' 0' << str;
2144
+ str = oss.str ();
2145
+ pos++;
2146
+ }
2147
+
2148
+ size_t last = 0 ;
2149
+ if (num_decimals == 0 ) {
2150
+ // Chop all decimal digits, including the decimal dot
2151
+ size_t pos2 = pos + 1 ;
2152
+ short n = str[pos2] - ' 0' ;
2153
+ if (n >= 5 ) {
2154
+ // Start rounding up - starting from the digit left of the dot all the way to the first digit
2155
+ bool carry_over = true ;
2156
+ for (short p = pos - 1 ; p >= 0 && carry_over; p--) {
2157
+ n = str[p] - ' 0' ;
2158
+ if (n == 9 ) {
2159
+ str[p] = ' 0' ;
2160
+ carry_over = true ;
2161
+ }
2162
+ else {
2163
+ n++;
2164
+ carry_over = false ;
2165
+ str[p] = ' 0' + n;
2166
+ }
2167
+ }
2168
+ if (carry_over) {
2169
+ std::ostringstream oss;
2170
+ oss << ' 1' << str.substr (0 , pos);
2171
+ str = oss.str ();
2172
+ pos++;
2173
+ }
2174
+ }
2175
+ last = pos;
2176
+ }
2177
+ else {
2178
+ size_t pos2 = pos + num_decimals + 1 ;
2179
+ // No need to check if rounding is necessary when pos2 has passed the last digit in the input string
2180
+ if (pos2 < str.length ()) {
2181
+ short n = str[pos2] - ' 0' ;
2182
+ if (n >= 5 ) {
2183
+ // Start rounding up - starting from the digit left of pos2 all the way to the first digit
2184
+ bool carry_over = true ;
2185
+ for (short p = pos2 - 1 ; p >= 0 && carry_over; p--) {
2186
+ if (str[p] == ' .' ) { // Skip the dot
2187
+ continue ;
2188
+ }
2189
+ n = str[p] - ' 0' ;
2190
+ if (n == 9 ) {
2191
+ str[p] = ' 0' ;
2192
+ carry_over = true ;
2193
+ }
2194
+ else {
2195
+ n++;
2196
+ carry_over = false ;
2197
+ str[p] = ' 0' + n;
2198
+ }
2199
+ }
2200
+ if (carry_over) {
2201
+ std::ostringstream oss;
2202
+ oss << ' 1' << str.substr (0 , pos2);
2203
+ str = oss.str ();
2204
+ pos2++;
2205
+ }
2206
+ }
2207
+ }
2208
+ last = pos2;
2209
+ }
2210
+
2211
+ // Add the minus sign back if negative
2212
+ if (isNegative) {
2213
+ std::ostringstream oss;
2214
+ oss << ' -' << str.substr (0 , last);
2215
+ str = oss.str ();
2216
+ } else {
2217
+ str = str.substr (0 , last);
2218
+ }
2219
+
2220
+ size_t len = str.length ();
2221
+ str.copy (field_value, len);
2222
+ field_value[len] = ' \0 ' ;
2223
+ *field_len = len;
2224
+ }
2082
2225
2083
2226
// To be called after all results are processed. ODBC and SQL Server do not guarantee that all output
2084
2227
// parameters will be present until all results are processed (since output parameters can depend on results
@@ -2160,6 +2303,11 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
2160
2303
core::sqlsrv_zval_stringl (value_z, str, str_len);
2161
2304
}
2162
2305
else {
2306
+ SQLSMALLINT decimal_digits = output_param->getDecimalDigits ();
2307
+ if (stmt->num_decimals >= 0 && decimal_digits >= 0 ) {
2308
+ format_decimal_numbers (stmt->num_decimals , decimal_digits, str, &str_len);
2309
+ }
2310
+
2163
2311
core::sqlsrv_zval_stringl (value_z, str, str_len);
2164
2312
}
2165
2313
}
@@ -2214,7 +2362,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
2214
2362
{
2215
2363
SQLRETURN r;
2216
2364
SQLSMALLINT c_type;
2217
- SQLLEN sql_field_type = 0 ;
2365
+ SQLSMALLINT sql_field_type = 0 ;
2218
2366
SQLSMALLINT extra = 0 ;
2219
2367
SQLLEN field_len_temp = 0 ;
2220
2368
SQLLEN sql_display_size = 0 ;
@@ -2425,6 +2573,10 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
2425
2573
throw core::CoreException ();
2426
2574
}
2427
2575
}
2576
+
2577
+ if (stmt->num_decimals >= 0 && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) {
2578
+ format_decimal_numbers (stmt->num_decimals , stmt->current_meta_data [field_index]->field_scale , field_value_temp, &field_len_temp);
2579
+ }
2428
2580
} // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE )
2429
2581
2430
2582
else {
0 commit comments