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: support extended string types #1043

Merged
merged 6 commits into from
Oct 23, 2019
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 .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ env:
- TEST_PHP_SQL_PWD=Password123

before_install:
- docker pull mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu
- docker pull mcr.microsoft.com/mssql/server:2019-CTP3.2-ubuntu

install:
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu
- docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP3.2-ubuntu
- docker build --build-arg PHPSQLDIR=$PHPSQLDIR -t msphpsql-dev -f Dockerfile-msphpsql .

before_script:
Expand Down
65 changes: 54 additions & 11 deletions source/pdo_sqlsrv/pdo_dbh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,8 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo
fetch_numeric( false ),
fetch_datetime( false ),
format_decimals( false ),
decimal_places( NO_CHANGE_DECIMAL_PLACES )
decimal_places( NO_CHANGE_DECIMAL_PLACES ),
use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED)
{
if( client_buffer_max_size < 0 ) {
client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT;
Expand Down Expand Up @@ -1104,6 +1105,27 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
}
break;

#if PHP_VERSION_ID >= 70200
case PDO_ATTR_DEFAULT_STR_PARAM:
{
if (Z_TYPE_P(val) != IS_LONG) {
THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID);
}

zend_long value = Z_LVAL_P(val);
if (value == PDO_PARAM_STR_NATL) {
driver_dbh->use_national_characters = 1;
}
else if (value == PDO_PARAM_STR_CHAR) {
driver_dbh->use_national_characters = 0;
}
else {
THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID);
}
}
break;
#endif

// Not supported
case PDO_ATTR_FETCH_TABLE_NAMES:
case PDO_ATTR_FETCH_CATALOG_NAMES:
Expand Down Expand Up @@ -1275,6 +1297,14 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
break;
}

#if PHP_VERSION_ID >= 70200
case PDO_ATTR_DEFAULT_STR_PARAM:
{
ZVAL_LONG(return_value, (driver_dbh->use_national_characters == 0) ? PDO_PARAM_STR_CHAR : PDO_PARAM_STR_NATL);
break;
}
#endif

default:
{
THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_INVALID_DBH_ATTR );
Expand Down Expand Up @@ -1425,14 +1455,18 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name,
// Return:
// 0 for failure, 1 for success.
int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const char* unquoted, _In_ size_t unquoted_len, _Outptr_result_buffer_(*quoted_len) char **quoted, _Out_ size_t* quoted_len,
enum pdo_param_type /*paramtype*/ TSRMLS_DC )
enum pdo_param_type paramtype TSRMLS_DC )
{
PDO_RESET_DBH_ERROR;
PDO_VALIDATE_CONN;
PDO_LOG_DBH_ENTRY;

SQLSRV_ENCODING encoding = SQLSRV_ENCODING_CHAR;

bool use_national_char_set = false;

pdo_sqlsrv_dbh* driver_dbh = static_cast<pdo_sqlsrv_dbh*>(dbh->driver_data);
SQLSRV_ASSERT(driver_dbh != NULL, "pdo_sqlsrv_dbh_quote: driver_data object was NULL.");

// get the current object in PHP; this distinguishes pdo_sqlsrv_dbh_quote being called from:
// 1. PDO::quote() - object name is PDO
// 2. PDOStatement::execute() - object name is PDOStatement
Expand Down Expand Up @@ -1461,13 +1495,12 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const
pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast<pdo_sqlsrv_stmt*>(stmt->driver_data);
SQLSRV_ASSERT(driver_stmt != NULL, "pdo_sqlsrv_dbh_quote: driver_data object was null");

if (driver_stmt->encoding() != SQLSRV_ENCODING_INVALID) {
encoding = driver_stmt->encoding();
}
else {
pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast<pdo_sqlsrv_dbh*>( stmt->driver_data );
encoding = driver_dbh->encoding();
encoding = driver_stmt->encoding();
if (encoding == SQLSRV_ENCODING_INVALID || encoding == SQLSRV_ENCODING_DEFAULT) {
pdo_sqlsrv_dbh* stmt_driver_dbh = reinterpret_cast<pdo_sqlsrv_dbh*>(stmt->driver_data);
encoding = stmt_driver_dbh->encoding();
}

// get the placeholder at the current position in driver_stmt->placeholders ht
// Normally it's not a good idea to alter the internal pointer in a hashed array
// (see pull request 634 on GitHub) but in this case this is for internal use only
Expand All @@ -1489,6 +1522,16 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const
}
}

use_national_char_set = (driver_dbh->use_national_characters == 1 || encoding == SQLSRV_ENCODING_UTF8);
#if PHP_VERSION_ID >= 70200
if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) {
use_national_char_set = true;
}
if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) {
use_national_char_set = false;
}
#endif

if ( encoding == SQLSRV_ENCODING_BINARY ) {
// convert from char* to hex digits using os
std::basic_ostringstream<char> os;
Expand Down Expand Up @@ -1533,7 +1576,7 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const
// count the number of quotes needed
unsigned int quotes_needed = 2; // the initial start and end quotes of course
// include the N proceeding the initial quote if encoding is UTF8
if ( encoding == SQLSRV_ENCODING_UTF8 ) {
if (use_national_char_set) {
quotes_needed = 3;
}
for ( size_t index = 0; index < unquoted_len; ++index ) {
Expand All @@ -1547,7 +1590,7 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const
unsigned int out_current = 0;

// insert N if the encoding is UTF8
if ( encoding == SQLSRV_ENCODING_UTF8 ) {
if (use_national_char_set) {
( *quoted )[out_current++] = 'N';
}
// insert initial quote
Expand Down
28 changes: 25 additions & 3 deletions source/pdo_sqlsrv/pdo_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1276,18 +1276,35 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt,
driver_stmt, PDO_SQLSRV_ERROR_INVALID_PARAM_DIRECTION, param->paramno + 1 ) {
throw pdo::PDOException();
}
// if the parameter is output or input/output, translate the type between the PDO::PARAM_* constant
// and the SQLSRV_PHPTYPE_* constant
// vso 2829: derive the pdo_type for input/output parameter as well
// also check if the user has specified PARAM_STR_NATL or PARAM_STR_CHAR for string params
int pdo_type = param->param_type;
if( param->max_value_len > 0 || param->max_value_len == SQLSRV_DEFAULT_SIZE ) {
if( param->param_type & PDO_PARAM_INPUT_OUTPUT ) {
direction = SQL_PARAM_INPUT_OUTPUT;
pdo_type = param->param_type & ~PDO_PARAM_INPUT_OUTPUT;
}
else {
direction = SQL_PARAM_OUTPUT;
}
}

// check if the user has specified the character set to use, take it off but ignore
#if PHP_VERSION_ID >= 70200
if ((pdo_type & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) {
pdo_type = pdo_type & ~PDO_PARAM_STR_NATL;
LOG(SEV_NOTICE, "PHP Extended String type PDO_PARAM_STR_NATL set but is ignored.");
}
if ((pdo_type & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) {
pdo_type = pdo_type & ~PDO_PARAM_STR_CHAR;
LOG(SEV_NOTICE, "PHP Extended String type PDO_PARAM_STR_CHAR set but is ignored.");
}
#endif

// if the parameter is output or input/output, translate the type between the PDO::PARAM_* constant
// and the SQLSRV_PHPTYPE_* constant
// vso 2829: derive the pdo_type for input/output parameter as well
int pdo_type = (direction == SQL_PARAM_OUTPUT) ? param->param_type : param->param_type & ~PDO_PARAM_INPUT_OUTPUT;
SQLSRV_PHPTYPE php_out_type = SQLSRV_PHPTYPE_INVALID;
switch (pdo_type) {
case PDO_PARAM_BOOL:
Expand Down Expand Up @@ -1354,13 +1371,17 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt,
driver_stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param->paramno + 1 ) {
throw pdo::PDOException();
}

// the encoding by default is that set on the statement
SQLSRV_ENCODING encoding = driver_stmt->encoding();
// if the statement's encoding is the default, then use the one on the connection
if( encoding == SQLSRV_ENCODING_DEFAULT ) {
encoding = driver_stmt->conn->encoding();
}
// if the user provided an encoding, use it instead

// Beginning with PHP7.2 the user can specify whether to use PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL
// But this extended type will be ignored in real prepared statements, so the encoding deliberately
// set in the statement or driver options will still take precedence
if( !Z_ISUNDEF(param->driver_params) ) {
CHECK_CUSTOM_ERROR( Z_TYPE( param->driver_params ) != IS_LONG, driver_stmt,
PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM ) {
Expand All @@ -1383,6 +1404,7 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt,
break;
}
}

// and bind the parameter
core_sqlsrv_bind_param( driver_stmt, static_cast<SQLUSMALLINT>( param->paramno ), direction, &(param->parameter) , php_out_type, encoding,
sql_type, column_size, decimal_digits TSRMLS_CC );
Expand Down
4 changes: 4 additions & 0 deletions source/pdo_sqlsrv/pdo_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ pdo_error PDO_ERRORS[] = {
SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED,
{ IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -96, true}
},
{
PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID,
{ IMSSP, (SQLCHAR*) "Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.", -97, false}
},

{ UINT_MAX, {} }
};
Expand Down
8 changes: 5 additions & 3 deletions source/pdo_sqlsrv/php_pdo_sqlsrv_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ class conn_string_parser : private string_parser
int discard_trailing_white_spaces( _In_reads_(len) const char* str, _Inout_ int len );
void validate_key( _In_reads_(key_len) const char *key, _Inout_ int key_len TSRMLS_DC);

protected:
void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC);
protected:
void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC);

public:
conn_string_parser( _In_ sqlsrv_context& ctx, _In_ const char* dsn, _In_ int len, _In_ HashTable* conn_options_ht );
Expand Down Expand Up @@ -183,6 +183,7 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn {
bool fetch_datetime;
bool format_decimals;
short decimal_places;
short use_national_characters;

pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC );
};
Expand Down Expand Up @@ -386,7 +387,8 @@ enum PDO_ERROR_CODES {
PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED,
PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION,
PDO_SQLSRV_ERROR_CE_DIRECT_QUERY_UNSUPPORTED,
PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED
PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED,
PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID
};

extern pdo_error PDO_ERRORS[];
Expand Down
3 changes: 3 additions & 0 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ const int SQL_SQLSTATE_BUFSIZE = SQL_SQLSTATE_SIZE + 1;
// default value of decimal places (no formatting required)
const short NO_CHANGE_DECIMAL_PLACES = -1;

// default value for national character set strings (user did not specify any preference)
const short CHARSET_PREFERENCE_NOT_SPECIFIED = -1;

// buffer size allocated to retrieve data from a PHP stream. This number
// was chosen since PHP doesn't return more than 8k at a time even if
// the amount requested was more.
Expand Down
109 changes: 109 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
--TEST--
GitHub issue 1018 - Test emulate prepared statements with the extended string types
--DESCRIPTION--
This test verifies the extended string types, PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL and
PDO::PARAM_STR_CHAR will affect "emulate prepared" statements. If the parameter encoding is specified,
it also matters. The N'' prefix will be used when either it is PDO::PARAM_STR_NATL or the
parameter encoding is UTF-8.
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif_old_php.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

$p = 'Ĉǽŋ';
$p1 = 'C??';

function toEmulatePrepare($conn, $pdoStrParam, $value, $testCase, $utf8 = false)
{
global $p;

$sql = 'SELECT :value';
$options = array(PDO::ATTR_EMULATE_PREPARES => true);
$stmt = $conn->prepare($sql, $options);

if ($utf8) {
$stmt->bindParam(':value', $p, $pdoStrParam, 0, PDO::SQLSRV_ENCODING_UTF8);
} else {
$stmt->bindParam(':value', $p, $pdoStrParam);
}
$stmt->execute();

$result = $stmt->fetch(PDO::FETCH_NUM);
trace("$testCase: expected $value and returned $result[0]\n");
if ($result[0] !== $value) {
echo("$testCase: expected $value but returned:\n");
var_dump($result);
}
}

try {
$conn = connect();

// Test case 1: PDO::PARAM_STR_NATL
$testCase = 'Test case 1: no default but specifies PDO::PARAM_STR_NATL';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase);

// Test case 2: PDO::PARAM_STR_CHAR
$testCase = 'Test case 2: no default but specifies PDO::PARAM_STR_CHAR';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase);

// Test case 3: no extended string types
$testCase = 'Test case 3: no default but no extended string types either';
toEmulatePrepare($conn, PDO::PARAM_STR, $p1, $testCase);

// Test case 4: no extended string types but specifies UTF 8 encoding
$testCase = 'Test case 4: no default but no extended string types but with UTF-8';
toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true);

////////////////////////////////////////////////////////////////////////
// NEXT tests: set the default string type: PDO::PARAM_STR_CHAR first
$conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_CHAR);

// Test case 5: overrides the default PDO::PARAM_STR_CHAR
$testCase = 'Test case 5: overrides the default PDO::PARAM_STR_CHAR';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase);

// Test case 6: specifies PDO::PARAM_STR_CHAR directly
$testCase = 'Test case 6: specifies PDO::PARAM_STR_CHAR, same as the default';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase);

// Test case 7: uses the default PDO::PARAM_STR_CHAR without specifying
$testCase = 'Test case 7: no extended string types (uses the default)';
toEmulatePrepare($conn, PDO::PARAM_STR, $p1, $testCase);

// Test case 8: uses the default PDO::PARAM_STR_CHAR without specifying but with UTF 8 encoding
$testCase = 'Test case 8: no extended string types (uses the default) but with UTF-8 ';
toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true);

////////////////////////////////////////////////////////////////////////
// NEXT tests: set the default string type: PDO::PARAM_STR_NATL
$conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL);

// Test case 9: overrides the default PDO::PARAM_STR_NATL
$testCase = 'Test case 9: overrides the default PDO::PARAM_STR_NATL';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase);

// Test case 10: specifies PDO::PARAM_STR_NATL directly
$testCase = 'Test case 10: specifies PDO::PARAM_STR_NATL, same as the default';
toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase);

// Test case 11: uses the default PDO::PARAM_STR_NATL without specifying
$testCase = 'Test case 11: no extended string types (uses the default)';
toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase);

// Test case 12: uses the default PDO::PARAM_STR_NATL without specifying but with UTF 8 encoding
$testCase = 'Test case 12: no extended string types (uses the default) but with UTF-8';
toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true);

echo "Done\n";
} catch (PdoException $e) {
echo $e->getMessage() . PHP_EOL;
}

?>
--EXPECT--
Done
Loading