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

Made changes to output param handling code to convert doubles to ints, if necessary #761

Merged
merged 6 commits into from
May 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions source/pdo_sqlsrv/pdo_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ pdo_error PDO_ERRORS[] = {
{ IMSSP, (SQLCHAR*) "Stored Procedures do not support text, ntext or image as OUTPUT parameters.", -83, false }
},
{
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
{ IMSSP, (SQLCHAR*) "Connection with Column Encryption enabled does not support fetching stream. Please fetch the data as a string.", -84, false }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is not needed anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's an obsolete error message that is no longer needed

SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED,
{ IMSSP, (SQLCHAR*) "Error converting a double (value out of range) to an integer.", -84, false }
},
{ UINT_MAX, {} }
};
Expand Down
14 changes: 8 additions & 6 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -1311,23 +1311,25 @@ 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
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;

// string output param constructor
sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) :
param_z( p_z ), encoding( enc ), param_num( num ), original_buffer_len( buffer_len ), is_bool( false )
param_z(p_z), encoding(enc), param_num(num), original_buffer_len(buffer_len), is_bool(false), php_out_type(SQLSRV_PHPTYPE_INVALID)
{
}

// every other type output parameter constructor
sqlsrv_output_param( _In_ zval* p_z, _In_ int num, _In_ bool is_bool ) :
sqlsrv_output_param( _In_ zval* p_z, _In_ int num, _In_ bool is_bool, _In_ SQLSRV_PHPTYPE php_out_type) :
param_z( p_z ),
encoding( SQLSRV_ENCODING_INVALID ),
param_num( num ),
original_buffer_len( -1 ),
is_bool( is_bool )
is_bool( is_bool ),
php_out_type(php_out_type)
{
}
};
Expand Down Expand Up @@ -1699,7 +1701,7 @@ enum SQLSRV_ERROR_CODES {
SQLSRV_ERROR_BUFFER_LIMIT_EXCEEDED,
SQLSRV_ERROR_INVALID_BUFFER_LIMIT,
SQLSRV_ERROR_OUTPUT_PARAM_TYPES_NOT_SUPPORTED,
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED,

// Driver specific error codes starts from here.
SQLSRV_ERROR_DRIVER_SPECIFIC = 1000,
Expand Down
23 changes: 19 additions & 4 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_
ind_ptr = buffer_len;
if( direction != SQL_PARAM_INPUT ){
// save the parameter so that 1) the buffer doesn't go away, and 2) we can set it to NULL if returned
sqlsrv_output_param output_param( param_ref, static_cast<int>( param_num ), zval_was_bool );
sqlsrv_output_param output_param( param_ref, static_cast<int>( param_num ), zval_was_bool, php_out_type);
save_output_param_for_later( stmt, output_param TSRMLS_CC );
}
}
Expand All @@ -503,7 +503,7 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_
ind_ptr = buffer_len;
if( direction != SQL_PARAM_INPUT ){
// save the parameter so that 1) the buffer doesn't go away, and 2) we can set it to NULL if returned
sqlsrv_output_param output_param( param_ref, static_cast<int>( param_num ), false );
sqlsrv_output_param output_param( param_ref, static_cast<int>( param_num ), zval_was_bool, php_out_type);
save_output_param_for_later( stmt, output_param TSRMLS_CC );
}
}
Expand Down Expand Up @@ -2149,8 +2149,23 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC )
break;
case IS_DOUBLE:
// for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so
if( stmt->param_ind_ptrs[ output_param->param_num ] == SQL_NULL_DATA ) {
ZVAL_NULL( value_z );
if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) {
ZVAL_NULL(value_z);
}
else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) {
// first check if its value is out of range
double dval = Z_DVAL_P(value_z);
if (dval > INT_MAX || dval < INT_MIN) {
CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) {
throw core::CoreException();
}
}
// if the output param is a boolean, still convert to
// a long integer first to take care of rounding
convert_to_long(value_z);
if (output_param->is_bool) {
convert_to_boolean(value_z);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that if the value as a double is 0.1, the corresponding boolean is false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

}
}
break;
default:
Expand Down
4 changes: 2 additions & 2 deletions source/sqlsrv/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,8 @@ ss_error SS_ERRORS[] = {
{ IMSSP, (SQLCHAR*) "Stored Procedures do not support text, ntext or image as OUTPUT parameters.", -108, false }
},
{
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
{ IMSSP, (SQLCHAR*) "Connection with Column Encryption enabled does not support fetching stream. Please fetch the data as a string.", -109, false }
SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED,
{ IMSSP, (SQLCHAR*)"Error converting a double (value out of range) to an integer.", -109, false }
},

// terminate the list of errors/warnings
Expand Down
141 changes: 141 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_707_ae_output_param_decimals.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
--TEST--
GitHub issue 707 - binding decimals/numerics to integers or booleans with ColumnEncryption
--DESCRIPTION--
Verifies that the double values will be rounded as integers or returned as booleans
The key of this test is to connect with ColumnEncryption enabled, and the table columns
do not need to be encrypted
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_unix.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

$error = "Error converting a double (value out of range) to an integer";

function getOutputs($stmt, $outSql, $id, $pdoParamType, $inout = false)
{
$dec = $num = 0;

if ($inout) {
$paramType = $pdoParamType | PDO::PARAM_INPUT_OUTPUT;
} else {
$paramType = $pdoParamType;
}

$stmt->bindParam(1, $id, PDO::PARAM_INT);
$stmt->bindParam(2, $dec, $paramType, PDO::SQLSRV_PARAM_OUT_DEFAULT_SIZE);
$stmt->bindParam(3, $num, $paramType, PDO::SQLSRV_PARAM_OUT_DEFAULT_SIZE);

$stmt->execute();

if ($pdoParamType == PDO::PARAM_BOOL) {
if (!$dec || !$num) {
echo "The returned booleans ($dec, $num) were unexpected!\n";
}
} else {
if ($dec != 100 || $num != 200) {
echo "The returned integers ($dec, $num) were unexpected!\n";
}
}
}

function getOutputsWithException($stmt, $outSql, $id, $pdoParamType, $inout = false)
{
global $error;

try {
getOutputs($stmt, $outSql, $id, $pdoParamType, $inout);
} catch (PDOException $e) {
$message = $e->getMessage();
$found = strpos($message, $error);
if ($found === false) {
echo "Exception message unexpected!\n";
}
}
}

function getSmallNumbers($conn, $outSql)
{
$stmt = $conn->prepare($outSql);
getOutputs($stmt, $outSql, 1, PDO::PARAM_BOOL);
getOutputs($stmt, $outSql, 1, PDO::PARAM_INT);

getOutputs($stmt, $outSql, 1, PDO::PARAM_BOOL, true);
getOutputs($stmt, $outSql, 1, PDO::PARAM_INT, true);

unset($stmt);
}

function getHugeNumbers($conn, $outSql)
{
// Expects an exception for each call
$stmt = $conn->prepare($outSql);

getOutputsWithException($stmt, $outSql, 2, PDO::PARAM_BOOL);
getOutputsWithException($stmt, $outSql, 2, PDO::PARAM_INT);

getOutputsWithException($stmt, $outSql, 2, PDO::PARAM_BOOL, true);
getOutputsWithException($stmt, $outSql, 2, PDO::PARAM_INT, true);

unset($stmt);
}

try {
// Check eligibility
$conn = new PDO( "sqlsrv:server = $server", $uid, $pwd );
if (!isAEQualified($conn)) {
echo "Done\n";
return;
}
unset($conn);

// Connection with column encryption enabled
$connectionInfo = "ColumnEncryption = Enabled;";
$conn = new PDO("sqlsrv:server = $server; database=$databaseName; $connectionInfo", $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$tableName = "test_707_decimals";
$procName = "sp_test_707_decimals";

dropTable($conn, $tableName);
dropProc($conn, $procName);

// Create a test table
$tsql = "CREATE TABLE $tableName (id int identity(1,1), c1_decimal decimal(19,4), c2_numeric numeric(20, 6))";
$stmt = $conn->query($tsql);
unset($stmt);

// Insert two rows
$tsql = "INSERT INTO $tableName (c1_decimal, c2_numeric) VALUES (100.078, 200.034)";
$stmt = $conn->query($tsql);
unset($stmt);

$tsql = "INSERT INTO $tableName (c1_decimal, c2_numeric) VALUES (199999999999.0123, 999243876923.09887)";
$stmt = $conn->query($tsql);
unset($stmt);

// Create a stored procedure
$procArgs = "@id int, @c_decimal decimal(19,4) OUTPUT, @c_numeric numeric(20, 6) OUTPUT";
$procCode = "SELECT @c_decimal = c1_decimal, @c_numeric = c2_numeric FROM $tableName WHERE id = @id";
createProc($conn, $procName, $procArgs, $procCode);

// Read them back by calling the stored procedure
$outSql = getCallProcSqlPlaceholders($procName, 3);
getSmallNumbers($conn, $outSql);
getHugeNumbers($conn, $outSql);

dropProc($conn, $procName);
dropTable($conn, $tableName);

unset($conn);
echo "Done\n";
} catch( PDOException $e ) {
print_r( $e->getMessage() );
}

?>
--EXPECT--
Done
24 changes: 10 additions & 14 deletions test/functional/pdo_sqlsrv/pdo_ae_output_param_decimals.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,15 @@ function compareFloats($actual, $expected)
function compareIntegers($det, $rand, $inputValues, $pdoParamType)
{
///////////////////////////////////////////////////////////////////////
// See GitHub issue 707 - Fix this method when the problem is addressed
//
// Assume $pdoParamType is PDO::PARAM_BOOL or PDO::PARAM_INT
if (is_string($det)) {
return (!compareFloats(floatval($det), $inputValues[0])
&& !compareFloats(floatval($rand), $inputValues[1]));
} else {
// if $pdoParamType is PDO::PARAM_BOOL,
// expect bool(true) or bool(false) depending on the rounded input values
// But with AE enabled (aforementioned GitHub issue), the fetched values
// are floats instead, which should be fixed
// if $pdoParamType is PDO::PARAM_BOOL, expect bool(true) or bool(false)
// depending on the rounded input values
$input0 = floor($inputValues[0]); // the positive float
$input1 = ceil($inputValues[1]); // the negative float
if (isAEConnected()) {
$det = boolval(floor($det));
$rand = boolval(ceil($rand));
}

return ($det == boolval($input0) && $rand == boolval($input1));
}
Expand Down Expand Up @@ -190,10 +182,14 @@ function testOutputDecimals($inout)
if ($found === false) {
printValues($errMsg, $det, $rand, $inputValues);
}
} elseif (!isAEConnected() && $precision >= 16) {
// When not AE enabled, large numbers are expected to
// fail when converting to booleans / integers
$error = "Error converting data type $dataType to int";
} elseif ($precision >= 16) {
// Large numbers are expected to fail when
// converting to booleans / integers
if (isAEConnected()) {
$error = "Error converting a double (value out of range) to an integer";
} else {
$error = "Error converting data type $dataType to int";
}
$found = strpos($message, $error);
if ($found === false) {
printValues($errMsg, $det, $rand, $inputValues);
Expand Down