Skip to content

Commit 18094a6

Browse files
authored
Feature request 415 for sqlsrv (#861)
1 parent 36fd97e commit 18094a6

File tree

6 files changed

+848
-27
lines changed

6 files changed

+848
-27
lines changed

source/shared/core_sqlsrv.h

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ enum SQLSRV_STMT_OPTIONS {
11071107
SQLSRV_STMT_OPTION_SCROLLABLE,
11081108
SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE,
11091109
SQLSRV_STMT_OPTION_DATE_AS_STRING,
1110+
SQLSRV_STMT_OPTION_FORMAT_DECIMALS,
11101111

11111112
// Driver specific connection options
11121113
SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000,
@@ -1296,6 +1297,11 @@ struct stmt_option_date_as_string : public stmt_option_functor {
12961297
virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC );
12971298
};
12981299

1300+
struct stmt_option_format_decimals : public stmt_option_functor {
1301+
1302+
virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC );
1303+
};
1304+
12991305
// used to hold the table for statment options
13001306
struct stmt_option {
13011307

@@ -1334,16 +1340,39 @@ extern php_stream_wrapper g_sqlsrv_stream_wrapper;
13341340
#define SQLSRV_STREAM_WRAPPER "sqlsrv"
13351341
#define SQLSRV_STREAM "sqlsrv_stream"
13361342

1343+
// *** parameter metadata struct ***
1344+
struct param_meta_data
1345+
{
1346+
SQLSMALLINT sql_type;
1347+
SQLSMALLINT decimal_digits;
1348+
SQLSMALLINT nullable;
1349+
SQLULEN column_size;
1350+
1351+
param_meta_data() : sql_type(0), decimal_digits(0), column_size(0), nullable(0)
1352+
{
1353+
}
1354+
1355+
~param_meta_data()
1356+
{
1357+
}
1358+
1359+
SQLSMALLINT get_sql_type() { return sql_type; }
1360+
SQLSMALLINT get_decimal_digits() { return decimal_digits; }
1361+
SQLSMALLINT get_nullable() { return nullable; }
1362+
SQLULEN get_column_size() { return column_size; }
1363+
};
1364+
13371365
// holds the output parameter information. Strings also need the encoding and other information for
13381366
// after processing. Only integer, float, and strings are allowable output parameters.
13391367
struct sqlsrv_output_param {
13401368

13411369
zval* param_z;
13421370
SQLSRV_ENCODING encoding;
1343-
SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement
1344-
SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer
1345-
SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary
1371+
SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement
1372+
SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer
1373+
SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary
13461374
bool is_bool;
1375+
param_meta_data meta_data; // parameter meta data
13471376

13481377
// string output param constructor
13491378
sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) :
@@ -1361,34 +1390,31 @@ struct sqlsrv_output_param {
13611390
php_out_type(php_out_type)
13621391
{
13631392
}
1364-
};
13651393

1366-
// forward decls
1367-
struct sqlsrv_result_set;
1368-
struct field_meta_data;
1369-
1370-
// *** parameter metadata struct ***
1371-
struct param_meta_data
1372-
{
1373-
SQLSMALLINT sql_type;
1374-
SQLSMALLINT decimal_digits;
1375-
SQLSMALLINT nullable;
1376-
SQLULEN column_size;
1377-
1378-
param_meta_data() : sql_type(0), decimal_digits(0), column_size(0), nullable(0)
1379-
{
1394+
void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE)
1395+
{
1396+
meta_data.sql_type = sql_type;
1397+
meta_data.column_size = column_size;
1398+
meta_data.decimal_digits = decimal_digits;
1399+
meta_data.nullable = nullable;
13801400
}
13811401

1382-
~param_meta_data()
1383-
{
1402+
SQLSMALLINT getDecimalDigits()
1403+
{
1404+
// Return decimal_digits only for decimal / numeric types. Otherwise, return -1
1405+
if (meta_data.sql_type == SQL_DECIMAL || meta_data.sql_type == SQL_NUMERIC) {
1406+
return meta_data.decimal_digits;
1407+
}
1408+
else {
1409+
return -1;
1410+
}
13841411
}
1385-
1386-
SQLSMALLINT get_sql_type() { return sql_type; }
1387-
SQLSMALLINT get_decimal_digits() { return decimal_digits; }
1388-
SQLSMALLINT get_nullable() { return nullable; }
1389-
SQLULEN get_column_size() { return column_size; }
13901412
};
13911413

1414+
// forward decls
1415+
struct sqlsrv_result_set;
1416+
struct field_meta_data;
1417+
13921418
// *** Statement resource structure ***
13931419
struct sqlsrv_stmt : public sqlsrv_context {
13941420

@@ -1409,6 +1435,7 @@ struct sqlsrv_stmt : public sqlsrv_context {
14091435
unsigned long query_timeout; // maximum allowed statement execution time
14101436
zend_long buffered_query_limit; // maximum allowed memory for a buffered query (measured in KB)
14111437
bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings
1438+
short num_decimals; // indicates number of decimals shown in fetched results (-1 by default, which means no formatting required)
14121439

14131440
// holds output pointers for SQLBindParameter
14141441
// We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving
@@ -1743,6 +1770,8 @@ enum SQLSRV_ERROR_CODES {
17431770
SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED,
17441771
SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN,
17451772
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
1773+
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
1774+
SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE,
17461775

17471776
// Driver specific error codes starts from here.
17481777
SQLSRV_ERROR_DRIVER_SPECIFIC = 1000,

source/shared/core_stmt.cpp

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_
107107
_Out_ SQLSMALLINT& sql_type TSRMLS_DC );
108108
void col_cache_dtor( _Inout_ zval* data_z );
109109
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);
110111
void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );
111112
void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type,
112113
_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
141142
past_next_result_end( false ),
142143
query_timeout( QUERY_TIMEOUT_INVALID ),
143144
date_as_string(false),
145+
num_decimals(-1), // -1 means no formatting required
144146
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
146148
send_streams_at_exec( true ),
147149
current_stream( NULL, SQLSRV_ENCODING_DEFAULT ),
148150
current_stream_read( 0 )
@@ -571,6 +573,8 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_
571573
// save the parameter to be adjusted and/or converted after the results are processed
572574
sqlsrv_output_param output_param( param_ref, encoding, param_num, static_cast<SQLUINTEGER>( buffer_len ) );
573575

576+
output_param.saveMetaData(sql_type, column_size, decimal_digits);
577+
574578
save_output_param_for_later( stmt, output_param TSRMLS_CC );
575579

576580
// 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
14161420
}
14171421
}
14181422

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+
14191438
// internal function to release the active stream. Called by each main API function
14201439
// that will alter the statement and cancel any retrieval of data from a stream.
14211440
void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
@@ -2079,6 +2098,130 @@ void field_cache_dtor( _Inout_ zval* data_z )
20792098
sqlsrv_free( cache );
20802099
}
20812100

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+
}
20822225

20832226
// To be called after all results are processed. ODBC and SQL Server do not guarantee that all output
20842227
// 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 )
21602303
core::sqlsrv_zval_stringl(value_z, str, str_len);
21612304
}
21622305
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+
21632311
core::sqlsrv_zval_stringl(value_z, str, str_len);
21642312
}
21652313
}
@@ -2214,7 +2362,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
22142362
{
22152363
SQLRETURN r;
22162364
SQLSMALLINT c_type;
2217-
SQLLEN sql_field_type = 0;
2365+
SQLSMALLINT sql_field_type = 0;
22182366
SQLSMALLINT extra = 0;
22192367
SQLLEN field_len_temp = 0;
22202368
SQLLEN sql_display_size = 0;
@@ -2425,6 +2573,10 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
24252573
throw core::CoreException();
24262574
}
24272575
}
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+
}
24282580
} // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE )
24292581

24302582
else {

source/sqlsrv/conn.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ namespace SSStmtOptionNames {
174174
const char SCROLLABLE[] = "Scrollable";
175175
const char CLIENT_BUFFER_MAX_SIZE[] = INI_BUFFERED_QUERY_LIMIT;
176176
const char DATE_AS_STRING[] = "ReturnDatesAsStrings";
177+
const char FORMAT_DECIMALS[] = "FormatDecimals";
177178
}
178179

179180
namespace SSConnOptionNames {
@@ -250,6 +251,12 @@ const stmt_option SS_STMT_OPTS[] = {
250251
SQLSRV_STMT_OPTION_DATE_AS_STRING,
251252
std::unique_ptr<stmt_option_date_as_string>( new stmt_option_date_as_string )
252253
},
254+
{
255+
SSStmtOptionNames::FORMAT_DECIMALS,
256+
sizeof( SSStmtOptionNames::FORMAT_DECIMALS ),
257+
SQLSRV_STMT_OPTION_FORMAT_DECIMALS,
258+
std::unique_ptr<stmt_option_format_decimals>( new stmt_option_format_decimals )
259+
},
253260
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
254261
};
255262

source/sqlsrv/util.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,14 @@ ss_error SS_ERRORS[] = {
428428
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
429429
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false}
430430
},
431+
{
432+
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
433+
{ IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -117, false}
434+
},
435+
{
436+
SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE,
437+
{ IMSSP, (SQLCHAR*) "For formatting decimal data values, %1!d! is out of range. Expected an integer from 0 to 38, inclusive.", -118, true}
438+
},
431439

432440
// terminate the list of errors/warnings
433441
{ UINT_MAX, {} }

0 commit comments

Comments
 (0)