Skip to content

Commit

Permalink
PDO::ATTR_EMULATE_PREPARES at the connection level (#1324)
Browse files Browse the repository at this point in the history
  • Loading branch information
yitam authored Nov 2, 2021
1 parent e3042e1 commit 3826f1a
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 43 deletions.
22 changes: 18 additions & 4 deletions source/pdo_sqlsrv/pdo_dbh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,8 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo
fetch_datetime( false ),
format_decimals( false ),
decimal_places( NO_CHANGE_DECIMAL_PLACES ),
use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED)
use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED),
emulate_prepare(false)
{
if( client_buffer_max_size < 0 ) {
client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT;
Expand Down Expand Up @@ -734,7 +735,8 @@ bool pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_ zend_string *sql_zstr,
// assign the methods for the statement object. This is necessary even if the
// statement fails so the user can retrieve the error information.
stmt->methods = &pdo_sqlsrv_stmt_methods;
stmt->supports_placeholders = PDO_PLACEHOLDER_POSITIONAL; // we support parameterized queries with ?, not names
// if not emulate_prepare, we support parameterized queries with ?, not names
stmt->supports_placeholders = (driver_dbh->emulate_prepare) ? PDO_PLACEHOLDER_NONE : PDO_PLACEHOLDER_POSITIONAL; // the statement options may override this later

// Initialize the options array to be passed to the core layer
ALLOC_HASHTABLE( pdo_stmt_options_ht );
Expand Down Expand Up @@ -1288,8 +1290,15 @@ bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_READ_ONLY_DBH_ATTR );
}

// Statement level only
case PDO_ATTR_EMULATE_PREPARES:
{
driver_dbh->emulate_prepare = zend_is_true(val);
if (driver_dbh->emulate_prepare && driver_dbh->ce_option.enabled) {
THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED);
}
}
break;
// Statement level only
case PDO_ATTR_CURSOR:
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
case SQLSRV_ATTR_DATA_CLASSIFICATION:
Expand Down Expand Up @@ -1362,8 +1371,13 @@ int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_
#endif
}

// Statement level only
case PDO_ATTR_EMULATE_PREPARES:
{
ZVAL_BOOL(return_value, driver_dbh->emulate_prepare);
break;
}

// Statement level only
case PDO_ATTR_CURSOR:
case SQLSRV_ATTR_CURSOR_SCROLL_TYPE:
case SQLSRV_ATTR_DATA_CLASSIFICATION:
Expand Down
1 change: 1 addition & 0 deletions source/pdo_sqlsrv/php_pdo_sqlsrv_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn {
bool format_decimals;
short decimal_places;
short use_national_characters;
bool emulate_prepare;

pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver );
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
--TEST--
Tests error returned when binding output parameter with emulate prepare
--DESCRIPTION--
The test shows that the option sets in prepared statements overrides the
connection setting of PDO::ATTR_EMULATE_PREPARES
--SKIPIF--
<?php require_once('skipif_mid-refactor.inc'); ?>
<?php require('skipif_azure_dw.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");
require_once("MsSetup.inc");

try {
$conn = connect();
// Do not connect with AE enabled because otherwise this would have thrown a different exception
$conn = new PDO("sqlsrv:server=$server; Database = $databaseName;", $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

$count = 0;

$query = "select ? = count(* ) from cd_info";
$stmt = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true));
$stmt->bindParam(1, $count, PDO::PARAM_STR, 10);
$stmt->execute();
echo "Result: ".$count."\n";

$query = "select bigint_type, int_type, money_type from [test_types] where int_type < 0";
$stmt1 = $conn->prepare($query);
$stmt1->execute();
$row = $stmt1->fetch(PDO::FETCH_ASSOC);
print_r($row);
$stmt = $conn->prepare($query);
} catch (PDOException $e) {
print("Error: " . $e->getMessage() . "\n");
}

try {
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$int = 0;
$bigint = 100;
$query = "select ? = bigint_type, ? = int_type, ? = money_type from [test_types] where int_type < 0";
$stmt2 = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true));
$stmt2->bindparam(1, $bigint, PDO::PARAM_STR, 256);
$stmt2->bindParam(2, $int, PDO::PARAM_INT, 4);
$stmt2->bindParam(3, $money, PDO::PARAM_STR, 1024);
$stmt2->execute();
echo "Big integer: ".$bigint."\n";
echo "Integer: ".$int."\n";
echo "Money: ".$money."\n";

//free the statement and connection
unset($stmt);
unset($stmt1);
unset($stmt2);
unset($conn);
$stmt = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true));
} catch (PDOException $e) {
print("Error: " . $e->getMessage() . "\n");
}

// free the statement and connection
unset($stmt);
unset($conn);
?>
--EXPECT--
Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters.
Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters.
4 changes: 2 additions & 2 deletions test/functional/pdo_sqlsrv/pdo_1310_null_varchar.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
GitHub issue 1310 - bind null field as varchar(max) if not binary
--DESCRIPTION--
The test shows null fields are no longer bound as char(1) if not binary such that it solves both issues 1310 and 1102.
Note that this test does not connect with AE enabled because SQLDescribeParam() does not work with these queries.
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

try {
$conn = connect();
$conn = new PDO("sqlsrv:server=$server; Database = $databaseName;", $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Issue 1310
Expand Down
62 changes: 62 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_1320_pdo_emulate_prepare.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
GitHub issue 1320 - support PDO::ATTR_EMULATE_PREPARES at the connection level
--DESCRIPTION--
Supports PDO::ATTR_EMULATE_PREPARES at the connection level but setting it to true with column
encryption enabled will fail with an exception. Also, the options in the prepared statement will
override the connection setting.
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

try {
// 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);

$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
echo "setAttribute should have failed because column encryption is enabled.\n\n";
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}

unset($conn);

try {
// Connection with column encryption enabled
$options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => true);
$connectionInfo = "ColumnEncryption = Enabled;";
$conn = new PDO("sqlsrv:server = $server; database=$databaseName; $connectionInfo", $uid, $pwd, $options);
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}

unset($conn);

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

echo "Connected successfully with column encryption enabled.\n";
$enabled = $conn->getAttribute(PDO::ATTR_EMULATE_PREPARES);
echo "By default, the emulation of prepared statements is:\n";
var_dump($enabled);
} catch (PDOException $e) {
echo $e->getMessage() . "\n";
}

?>

--EXPECT--
SQLSTATE[IMSSP]: Parameterized statement with attribute PDO::ATTR_EMULATE_PREPARES is not supported in a Column Encryption enabled Connection.
SQLSTATE[IMSSP]: Parameterized statement with attribute PDO::ATTR_EMULATE_PREPARES is not supported in a Column Encryption enabled Connection.
Connected successfully with column encryption enabled.
By default, the emulation of prepared statements is:
bool(false)
137 changes: 137 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
--TEST--
Prepare with emulate prepare and binding uft8 characters
--DESCRIPTION--
This is the same as pdo_prepare_emulatePrepare_unicode.phpt except that
PDO::ATTR_EMULATE_PREPARES at the connection level
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once('MsCommon_mid-refactor.inc');

function prepareStmt($conn, $query, $prepareOptions = array(), $dataType = PDO::PARAM_STR, $length = 0, $driverOptions = null)
{
$name = "가각";
if (!isColEncrypted()) {
$stmt = $conn->prepare($query, $prepareOptions);
$stmt->bindParam(':name', $name, $dataType, $length, $driverOptions);
} else {
$status = 1;
$stmt = $conn->prepare($query, $prepareOptions);
$stmt->bindParam(':name', $name, $dataType, $length, $driverOptions);
$stmt->bindParam(':status', $status);
}
$stmt->execute();
return $stmt;
}

try {
$conn = connect("", array(), PDO::ERRMODE_SILENT);

$tableName = "users";
createTable($conn, $tableName, array("name" => "nvarchar(max)", "status" => "int", "age" => "int"));

if (!isColEncrypted()) {
$conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'Belle', 1, 34)");
$conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'Абрам', 1, 40)");
$conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'가각', 1, 30)");
$query = "SELECT * FROM [$tableName] WHERE name = :name AND status = 1";
} else {
insertRow($conn, $tableName, array("name" => "Belle", "status" => 1, "age" => 34));
insertRow($conn, $tableName, array("name" => "Абрам", "status" => 1, "age" => 40));
insertRow($conn, $tableName, array("name" => "가각", "status" => 1, "age" => 30));
$query = "SELECT * FROM [$tableName] WHERE name = :name AND status = :status";
}

//without emulate prepare
print_r("Prepare without emulate prepare:\n");
$stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($row);

if (!isAEConnected()) {
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
} else {
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}

//with emulate prepare and no bind param options
print_r("Prepare with emulate prepare and no bindParam options:\n");
// This test only makes sense without AE because the default encoding is PDO::SQLSRV_ENCODING_UTF8
if (!isAEConnected()) {
$stmt = prepareStmt($conn, $query, array());
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($stmt->rowCount() != 0) {
print_r("Do not expect results for this query!\n");
print_r($row);
}
}

//with emulate prepare and SQLSRV_ENCODING_UTF8
print_r("Prepare with emulate prepare and SQLSRV_ENCODING_UTF8:\n");
$stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($row);

//with emulate prepare and SQLSRV_ENCODING_SYSTEM
print_r("Prepare with emulate prepare and and SQLSRV_ENCODING_SYSTEM:\n");
$stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_SYSTEM);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

// The combination of Column Encryption and Unix platforms support SQLSRV_ENCODING_SYSTEM because:
// With Column Encryption enabled, binding parameters uses exact datatypes as the column definition
// the default encoding in Linux and Mac is UTF8
$success = true;
if (!(strtoupper( substr( php_uname( 's' ),0,3 ) ) === 'WIN') && isAEConnected()) {
if ($row['name'] != "가각" || $row['status'] != 1 || $row['age'] != 30) {
print_r("Incorrect results retrieved.\n");
$success = false;
}
} else {
// the default encoding in Windows is non-UTF8, thus binding UTF8 parameters does not work
if ($stmt->rowCount() != 0) {
print_r("Binding UTF8 data when encoding is SQLSRV_ENCODING_SYSTEM should not work.\n");
$success = false;
}
}
if ($success) {
print_r("Binding UTF8 data with SQLSRV_ENCODING_SYSTEM is tested successfully.\n");
}

//with emulate prepare and encoding SQLSRV_ENCODING_BINARY
print_r("Prepare with emulate prepare and encoding SQLSRV_ENCODING_BINARY:\n");
$stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_BINARY);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($row);
if ($stmt->rowCount() == 0) {
print_r("No results for this query\n");
}

dropTable($conn, $tableName);
unset($stmt);
unset($conn);
} catch (PDOException $e) {
var_dump($e->errorInfo);
}
?>

--EXPECT--
Prepare without emulate prepare:
Array
(
[name] => 가각
[status] => 1
[age] => 30
)
Prepare with emulate prepare and no bindParam options:
Prepare with emulate prepare and SQLSRV_ENCODING_UTF8:
Array
(
[name] => 가각
[status] => 1
[age] => 30
)
Prepare with emulate prepare and and SQLSRV_ENCODING_SYSTEM:
Binding UTF8 data with SQLSRV_ENCODING_SYSTEM is tested successfully.
Prepare with emulate prepare and encoding SQLSRV_ENCODING_BINARY:
No results for this query
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,6 @@ try {
$tableName = "pdo_test_table";
createTable($conn1, $tableName, array(new ColumnMeta("int", "ID", "NOT NULL PRIMARY KEY"), "Policy" => "varchar(2)", "Label" => "varchar(10)", "Budget" => "money"));

try {
$res = $conn1->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
if ($res) {
echo "setAttribute should have failed.\n\n";
}
} catch (Exception $e) {
echo $e->getMessage() . "\n";
}

try {
$query = "SELECT * FROM [$tableName]";
$stmt = $conn1->query($query);
Expand Down Expand Up @@ -186,7 +177,6 @@ try {

?>
--EXPECTF--
SQLSTATE[IMSSP]: The given attribute is only supported on the PDOStatement object.
SQLSTATE[IMSSP]: An invalid attribute was designated on the PDOStatement object.
Start inserting data...
....Done....
Expand Down
3 changes: 2 additions & 1 deletion test/functional/sqlsrv/srv_1310_null_varchar.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
GitHub issue 1310 - bind null field as varchar(max) if not binary
--DESCRIPTION--
The test shows null fields are no longer bound as char(1) if not binary such that it solves both issues 1310 and 1102.
Note that this test does not connect with AE enabled because SQLDescribeParam() does not work with these queries.
--ENV--
PHPT_EXEC=true
--SKIPIF--
Expand All @@ -10,7 +11,7 @@ PHPT_EXEC=true
<?php
require_once('MsCommon.inc');

$conn = AE\connect();
$conn = connect();

// Issue 1310
$query = "SELECT CAST(ISNULL(?, -1) AS INT) AS K";
Expand Down

0 comments on commit 3826f1a

Please sign in to comment.