diff --git a/.travis.yml b/.travis.yml index 0618bebf..d908eff6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 15aec117..eedd07b1 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -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; @@ -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: @@ -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 ); @@ -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(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 @@ -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(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( 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(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 @@ -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 os; @@ -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 ) { @@ -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 diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 0cd562d9..c28aa1bc 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -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: @@ -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 ) { @@ -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( param->paramno ), direction, &(param->parameter) , php_out_type, encoding, sql_type, column_size, decimal_digits TSRMLS_CC ); diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 6cfb43ac..cff7add5 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -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, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index e0f5f220..2b7269c0 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -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 ); @@ -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 ); }; @@ -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[]; diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index f468a7b6..28f46f68 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -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. diff --git a/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt new file mode 100644 index 00000000..5c389bb9 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt @@ -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-- + +--FILE-- + 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 diff --git a/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt new file mode 100644 index 00000000..5b39f9f1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt @@ -0,0 +1,93 @@ +--TEST-- +GitHub issue 1018 - Test PDO::quote() 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 how PDO::quote() works. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +query('select 1'); + $error = '*An invalid attribute was designated on the PDOStatement object.'; + $pdoParam = ($isChar) ? PDO::PARAM_STR_CHAR : PDO::PARAM_STR_NATL; + + // This will cause an exception because PDO::ATTR_DEFAULT_STR_PARAM is not a statement attribute + $stmt->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, $pdoParam); + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Unexpected error returned setting PDO::ATTR_DEFAULT_STR_PARAM on statement\n"; + var_dump($e->getMessage()); + } + } +} + +function testErrorCase($attr) +{ + try { + $conn = connect(); + $error = '*Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.'; + + // This will cause an exception because PDO::ATTR_DEFAULT_STR_PARAM expects either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL only + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, $attr); + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Unexpected error returned setting PDO::ATTR_DEFAULT_STR_PARAM\n"; + var_dump($e->getMessage()); + } + } +} + +try { + testErrorCase(true); + testErrorCase('abc'); + testErrorCase(4); + + $conn = connect(); + testErrorCase2($conn, true); + testErrorCase2($conn, false); + + // Start testing quote function + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_CHAR); + + var_dump($conn->quote(null, PDO::PARAM_NULL)); + var_dump($conn->quote('\'', PDO::PARAM_STR)); + var_dump($conn->quote('foo', PDO::PARAM_STR)); + var_dump($conn->quote('foo', PDO::PARAM_STR | PDO::PARAM_STR_CHAR)); + var_dump($conn->quote('über', PDO::PARAM_STR | PDO::PARAM_STR_NATL)); + + var_dump($conn->getAttribute(PDO::ATTR_DEFAULT_STR_PARAM) === PDO::PARAM_STR_CHAR); + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL); + var_dump($conn->getAttribute(PDO::ATTR_DEFAULT_STR_PARAM) === PDO::PARAM_STR_NATL); + + var_dump($conn->quote('foo', PDO::PARAM_STR | PDO::PARAM_STR_CHAR)); + var_dump($conn->quote('über', PDO::PARAM_STR)); + var_dump($conn->quote('über', PDO::PARAM_STR | PDO::PARAM_STR_NATL)); + + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +string(2) "''" +string(4) "''''" +string(5) "'foo'" +string(5) "'foo'" +string(8) "N'über'" +bool(true) +bool(true) +string(5) "'foo'" +string(8) "N'über'" +string(8) "N'über'" +Done diff --git a/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt new file mode 100644 index 00000000..e058994e --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt @@ -0,0 +1,131 @@ +--TEST-- +GitHub issue 1018 - Test real 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 NOT affect real prepared statements. Unlike emulate prepared statements, +real prepared statements will only be affected by the parameter encoding. If not set, it will use +the statement encoding or the connection one, which is by default UTF-8. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + false); // it's false by default anyway + $stmt = $conn->prepare($sql, $options); + + // Set param encoding only if $encoding is NOT FALSE + if ($encoding !== false) { + $stmt->bindParam(':value', $p, $pdoStrParam, 0, $encoding); + $encOptions = array(PDO::SQLSRV_ATTR_ENCODING => $encoding); + } else { + $stmt->bindParam(':value', $p, $pdoStrParam); + $encOptions = array(); + } + $stmt->execute(); + + // Should also set statement encoding when $encoding is NOT FALSE + // such that data can be fetched with the right encoding + $sql = "SELECT Col1 FROM $tableName WHERE ID = $id"; + $stmt = $conn->prepare($sql, $encOptions); + $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); + } +} + +function testUTF8encoding($conn) +{ + global $p, $tableName; + + // Create a NVARCHAR column + $sql = "CREATE TABLE $tableName (ID int identity(1,1), Col1 NVARCHAR(100))"; + $conn->query($sql); + + // The extended string types PDO::PARAM_STR_NATL and PDO::PARAM_STR_CHAR + // will be ignored in the following test cases. Only the statement or + // the connection encoding matters. + + // Test case 1: PDO::PARAM_STR_CHAR + $testCase = 'UTF-8 case 1: no default but specifies PDO::PARAM_STR_CHAR'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p, $testCase, 1); + + // Test case 2: PDO::PARAM_STR_NATL + $testCase = 'UTF-8 case 2: no default but specifies PDO::PARAM_STR_NATL'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase, 2); + + // Test case 3: no extended string types + $testCase = 'UTF-8 case 3: no default but no extended string types either'; + insertRead($conn, PDO::PARAM_STR, $p, $testCase, 3); + + // Test case 4: no extended string types but specifies UTF-8 encoding + $testCase = 'UTF-8 case 4: no default but no extended string types but with UTF-8 encoding'; + insertRead($conn, PDO::PARAM_STR, $p, $testCase, 4, PDO::SQLSRV_ENCODING_UTF8); + + dropTable($conn, $tableName); +} + +function testNonUTF8encoding($conn) +{ + global $p, $p1, $tableName; + + // Create a VARCHAR column + $sql = "CREATE TABLE $tableName (ID int identity(1,1), Col1 VARCHAR(100))"; + $conn->query($sql); + + // The extended string types PDO::PARAM_STR_NATL and PDO::PARAM_STR_CHAR + // will be ignored in the following test cases. Only the statement or + // the connection encoding matters. + + // Test case 1: PDO::PARAM_STR_CHAR (expect $p1) + $testCase = 'System case 1: no default but specifies PDO::PARAM_STR_CHAR'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase, 1); + + // Test case 2: PDO::PARAM_STR_NATL (expect $p1) + $testCase = 'System case 2: no default but specifies PDO::PARAM_STR_NATL'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p1, $testCase, 2); + + // Test case 3: no extended string types (expect $p1) + $testCase = 'System case 3: no default but no extended string types either'; + insertRead($conn, PDO::PARAM_STR, $p1, $testCase, 3); + + // Test case 4: no extended string types but specifies UTF-8 encoding (expect $p1) + $testCase = 'System case 4: no default but no extended string types but with UTF-8 encoding'; + insertRead($conn, PDO::PARAM_STR, $p1, $testCase, 4, PDO::SQLSRV_ENCODING_UTF8); + + dropTable($conn, $tableName); +} + +try { + $conn = connect(); + dropTable($conn, $tableName); + + // The connection encoding is by default PDO::SQLSRV_ENCODING_UTF8. For this test + // no change is made to the connection encoding. + testUTF8encoding($conn); + testNonUTF8encoding($conn); + + echo "Done\n"; +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/skipif_old_php.inc b/test/functional/pdo_sqlsrv/skipif_old_php.inc new file mode 100644 index 00000000..19f97bc5 --- /dev/null +++ b/test/functional/pdo_sqlsrv/skipif_old_php.inc @@ -0,0 +1,10 @@ +