Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request 415 for sqlsrv #861

Merged
merged 14 commits into from
Oct 12, 2018
Merged
79 changes: 54 additions & 25 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,7 @@ enum SQLSRV_STMT_OPTIONS {
SQLSRV_STMT_OPTION_SCROLLABLE,
SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE,
SQLSRV_STMT_OPTION_DATE_AS_STRING,
SQLSRV_STMT_OPTION_FORMAT_DECIMALS,

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

struct stmt_option_format_decimals : public stmt_option_functor {

virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC );
};

// used to hold the table for statment options
struct stmt_option {

Expand Down Expand Up @@ -1334,16 +1340,39 @@ extern php_stream_wrapper g_sqlsrv_stream_wrapper;
#define SQLSRV_STREAM_WRAPPER "sqlsrv"
#define SQLSRV_STREAM "sqlsrv_stream"

// *** parameter metadata struct ***
struct param_meta_data
{
SQLSMALLINT sql_type;
SQLSMALLINT decimal_digits;
SQLSMALLINT nullable;
SQLULEN column_size;

param_meta_data() : sql_type(0), decimal_digits(0), column_size(0), nullable(0)
{
}

~param_meta_data()
{
}

SQLSMALLINT get_sql_type() { return sql_type; }
SQLSMALLINT get_decimal_digits() { return decimal_digits; }
SQLSMALLINT get_nullable() { return nullable; }
SQLULEN get_column_size() { return column_size; }
};

// holds the output parameter information. Strings also need the encoding and other information for
// after processing. Only integer, float, and strings are allowable output parameters.
struct sqlsrv_output_param {

zval* param_z;
SQLSRV_ENCODING encoding;
SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement
SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer
SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary
SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement
SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer
SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary
bool is_bool;
param_meta_data meta_data; // parameter meta data

// string output param constructor
sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) :
Expand All @@ -1361,34 +1390,31 @@ struct sqlsrv_output_param {
php_out_type(php_out_type)
{
}
};

// forward decls
struct sqlsrv_result_set;
struct field_meta_data;

// *** parameter metadata struct ***
struct param_meta_data
{
SQLSMALLINT sql_type;
SQLSMALLINT decimal_digits;
SQLSMALLINT nullable;
SQLULEN column_size;

param_meta_data() : sql_type(0), decimal_digits(0), column_size(0), nullable(0)
{
void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE)
{
meta_data.sql_type = sql_type;
meta_data.column_size = column_size;
meta_data.decimal_digits = decimal_digits;
meta_data.nullable = nullable;
}

~param_meta_data()
{
SQLSMALLINT getDecimalDigits()
{
// Return decimal_digits only for decimal / numeric types. Otherwise, return -1
if (meta_data.sql_type == SQL_DECIMAL || meta_data.sql_type == SQL_NUMERIC) {
return meta_data.decimal_digits;
}
else {
return -1;
}
}

SQLSMALLINT get_sql_type() { return sql_type; }
SQLSMALLINT get_decimal_digits() { return decimal_digits; }
SQLSMALLINT get_nullable() { return nullable; }
SQLULEN get_column_size() { return column_size; }
};

// forward decls
struct sqlsrv_result_set;
struct field_meta_data;

// *** Statement resource structure ***
struct sqlsrv_stmt : public sqlsrv_context {

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

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

// Driver specific error codes starts from here.
SQLSRV_ERROR_DRIVER_SPECIFIC = 1000,
Expand Down
156 changes: 154 additions & 2 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_
_Out_ SQLSMALLINT& sql_type TSRMLS_DC );
void col_cache_dtor( _Inout_ zval* data_z );
void field_cache_dtor( _Inout_ zval* data_z );
void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len);
void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );
void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type,
_Inout_updates_bytes_(*field_len) void*& field_value, _Inout_ SQLLEN* field_len TSRMLS_DC );
Expand Down Expand Up @@ -141,8 +142,9 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error
past_next_result_end( false ),
query_timeout( QUERY_TIMEOUT_INVALID ),
date_as_string(false),
num_decimals(-1), // -1 means no formatting required
buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ),
param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte
param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte
send_streams_at_exec( true ),
current_stream( NULL, SQLSRV_ENCODING_DEFAULT ),
current_stream_read( 0 )
Expand Down Expand Up @@ -571,6 +573,8 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_
// save the parameter to be adjusted and/or converted after the results are processed
sqlsrv_output_param output_param( param_ref, encoding, param_num, static_cast<SQLUINTEGER>( buffer_len ) );

output_param.saveMetaData(sql_type, column_size, decimal_digits);

save_output_param_for_later( stmt, output_param TSRMLS_CC );

// For output parameters, if we set the column_size to be same as the buffer_len,
Expand Down Expand Up @@ -1416,6 +1420,21 @@ void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op
}
}

void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC )
{
// first check if the input is an integer
CHECK_CUSTOM_ERROR(Z_TYPE_P(value_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_FORMAT_DECIMALS) {
throw core::CoreException();
}

zend_long format_decimals = Z_LVAL_P(value_z);
CHECK_CUSTOM_ERROR(format_decimals < 0 || format_decimals > SQL_SERVER_MAX_PRECISION, stmt, SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE, format_decimals) {
throw core::CoreException();
}

stmt->num_decimals = static_cast<short>(format_decimals);
}

// internal function to release the active stream. Called by each main API function
// that will alter the statement and cancel any retrieval of data from a stream.
void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
Expand Down Expand Up @@ -2079,6 +2098,130 @@ void field_cache_dtor( _Inout_ zval* data_z )
sqlsrv_free( cache );
}

// To be called for formatting decimal / numeric fetched values from finalize_output_parameters() and/or get_field_as_string()
void format_decimal_numbers(_In_ SQLSMALLINT decimals_digits, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len)
{
// In SQL Server, the default maximum precision of numeric and decimal data types is 38
//
// Note: stmt->num_decimals is -1 by default, which means no formatting on decimals / numerics is necessary
// If the required number of decimals is larger than the field scale, will use the column field scale instead.
// This is to ensure the number of decimals adheres to the column field scale. If smaller, the output value may be rounded up.
//
// Note: it's possible that the decimal / numeric value does not contain a decimal dot because the field scale is 0.
// Thus, first check if the decimal dot exists. If not, no formatting necessary, regardless of decimals_digits
//
std::string str = field_value;
size_t pos = str.find_first_of('.');

if (pos == std::string::npos || decimals_digits < 0) {
return;
}

SQLSMALLINT num_decimals = decimals_digits;
if (num_decimals > field_scale) {
num_decimals = field_scale;
}

// We want the rounding to be consistent with php number_format(), http://php.net/manual/en/function.number-format.php
// as well as SQL Server Management studio, such that the least significant digit will be rounded up if it is
// followed by 5 or above.

bool isNegative = false;

// If negative, remove the minus sign for now so as not to complicate the rounding process
if (str[0] == '-') {
isNegative = true;
std::ostringstream oss;
oss << str.substr(1);
str = oss.str();
pos = str.find_first_of('.');
}

// Adds the leading zero if not exists
if (pos == 0) {
std::ostringstream oss;
oss << '0' << str;
str = oss.str();
pos++;
}

size_t last = 0;
if (num_decimals == 0) {
// Chop all decimal digits, including the decimal dot
size_t pos2 = pos + 1;
short n = str[pos2] - '0';
if (n >= 5) {
// Start rounding up - starting from the digit left of the dot all the way to the first digit
bool carry_over = true;
for (short p = pos - 1; p >= 0 && carry_over; p--) {
n = str[p] - '0';
if (n == 9) {
str[p] = '0' ;
carry_over = true;
}
else {
n++;
carry_over = false;
str[p] = '0' + n;
}
}
if (carry_over) {
std::ostringstream oss;
oss << '1' << str.substr(0, pos);
str = oss.str();
pos++;
}
}
last = pos;
}
else {
size_t pos2 = pos + num_decimals + 1;
// No need to check if rounding is necessary when pos2 has passed the last digit in the input string
if (pos2 < str.length()) {
short n = str[pos2] - '0';
if (n >= 5) {
// Start rounding up - starting from the digit left of pos2 all the way to the first digit
bool carry_over = true;
for (short p = pos2 - 1; p >= 0 && carry_over; p--) {
if (str[p] == '.') { // Skip the dot
continue;
}
n = str[p] - '0';
if (n == 9) {
str[p] = '0' ;
carry_over = true;
}
else {
n++;
carry_over = false;
str[p] = '0' + n;
}
}
if (carry_over) {
std::ostringstream oss;
oss << '1' << str.substr(0, pos2);
str = oss.str();
pos2++;
}
}
}
last = pos2;
}

// Add the minus sign back if negative
if (isNegative) {
std::ostringstream oss;
oss << '-' << str.substr(0, last);
str = oss.str();
} else {
str = str.substr(0, last);
}

size_t len = str.length();
str.copy(field_value, len);
field_value[len] = '\0';
*field_len = len;
}

// To be called after all results are processed. ODBC and SQL Server do not guarantee that all output
// parameters will be present until all results are processed (since output parameters can depend on results
Expand Down Expand Up @@ -2160,6 +2303,11 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
core::sqlsrv_zval_stringl(value_z, str, str_len);
}
else {
SQLSMALLINT decimal_digits = output_param->getDecimalDigits();
if (stmt->num_decimals >= 0 && decimal_digits >= 0) {
format_decimal_numbers(stmt->num_decimals, decimal_digits, str, &str_len);
}

core::sqlsrv_zval_stringl(value_z, str, str_len);
}
}
Expand Down Expand Up @@ -2214,7 +2362,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
{
SQLRETURN r;
SQLSMALLINT c_type;
SQLLEN sql_field_type = 0;
SQLSMALLINT sql_field_type = 0;
SQLSMALLINT extra = 0;
SQLLEN field_len_temp = 0;
SQLLEN sql_display_size = 0;
Expand Down Expand Up @@ -2425,6 +2573,10 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind
throw core::CoreException();
}
}

if (stmt->num_decimals >= 0 && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) {
format_decimal_numbers(stmt->num_decimals, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp);
}
} // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE )

else {
Expand Down
7 changes: 7 additions & 0 deletions source/sqlsrv/conn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ namespace SSStmtOptionNames {
const char SCROLLABLE[] = "Scrollable";
const char CLIENT_BUFFER_MAX_SIZE[] = INI_BUFFERED_QUERY_LIMIT;
const char DATE_AS_STRING[] = "ReturnDatesAsStrings";
const char FORMAT_DECIMALS[] = "FormatDecimals";
}

namespace SSConnOptionNames {
Expand Down Expand Up @@ -250,6 +251,12 @@ const stmt_option SS_STMT_OPTS[] = {
SQLSRV_STMT_OPTION_DATE_AS_STRING,
std::unique_ptr<stmt_option_date_as_string>( new stmt_option_date_as_string )
},
{
SSStmtOptionNames::FORMAT_DECIMALS,
sizeof( SSStmtOptionNames::FORMAT_DECIMALS ),
SQLSRV_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>{} },
};

Expand Down
8 changes: 8 additions & 0 deletions source/sqlsrv/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ ss_error SS_ERRORS[] = {
SQLSRV_ERROR_EMPTY_ACCESS_TOKEN,
{ IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false}
},
{
SQLSRV_ERROR_INVALID_FORMAT_DECIMALS,
{ IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -117, false}
},
{
SQLSRV_ERROR_FORMAT_DECIMALS_OUT_OF_RANGE,
{ IMSSP, (SQLCHAR*) "For formatting decimal data values, %1!d! is out of range. Expected an integer from 0 to 38, inclusive.", -118, true}
},

// terminate the list of errors/warnings
{ UINT_MAX, {} }
Expand Down
Loading