From 5f76f838e5d44ca951a44bec6be1af05fae4bd93 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Wed, 15 Apr 2020 13:49:52 -0700 Subject: [PATCH] 5.8.1 hotfix release (#1126) --- CHANGELOG.md | 24 + Linux-mac-install.md | 18 +- README.md | 2 +- appveyor.yml | 4 +- azure-pipelines.yml | 4 +- codecov.yml | 21 + source/pdo_sqlsrv/config.m4 | 11 +- source/pdo_sqlsrv/pdo_dbh.cpp | 21 +- source/pdo_sqlsrv/pdo_init.cpp | 16 +- source/pdo_sqlsrv/pdo_stmt.cpp | 19 +- source/pdo_sqlsrv/pdo_util.cpp | 25 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 2 +- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 10 +- source/shared/core_results.cpp | 101 +- source/shared/core_sqlsrv.h | 17 +- source/shared/core_stmt.cpp | 6 +- source/shared/core_util.cpp | 39 +- source/shared/localization.hpp | 3 + source/shared/localizationimpl.cpp | 108 +- source/shared/version.h | 2 +- source/sqlsrv/config.m4 | 20 +- source/sqlsrv/conn.cpp | 15 +- source/sqlsrv/init.cpp | 4 +- source/sqlsrv/php_sqlsrv_int.h | 12 +- source/sqlsrv/stmt.cpp | 3 +- source/sqlsrv/util.cpp | 28 +- test/bvt/sqlsrv/break.inc | 15 +- test/bvt/sqlsrv/connect.inc | 2 + test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt | 5 +- .../msdn_sqlsrv_get_field_stream_binary.php | 4 +- .../sqlsrv/msdn_sqlsrv_query_scrollable.phpt | 5 +- test/extended/AE_v2_values.inc | 220 ++++ test/extended/MsSetup.inc | 58 + test/extended/pdo_AE_functions.inc | 911 ++++++++++++++++ .../pdo_aev2_plaintext_nonstring.phpt | 41 + test/extended/pdo_aev2_plaintext_string.phpt | 41 + ...do_aev2_reencrypt_encrypted_nonstring.phpt | 39 + .../pdo_aev2_reencrypt_encrypted_string.phpt | 39 + test/extended/skipif_not_hgs.inc | 36 + test/extended/sqlsrv_AE_functions.inc | 991 ++++++++++++++++++ .../sqlsrv_aev2_plaintext_nonstring.phpt | 41 + .../sqlsrv_aev2_plaintext_string.phpt | 41 + ...rv_aev2_reencrypt_encrypted_nonstring.phpt | 39 + ...qlsrv_aev2_reencrypt_encrypted_string.phpt | 39 + test/functional/pdo_sqlsrv/break_pdo.php | 22 +- .../pdo_sqlsrv/pdo_ae_insert_numeric.phpt | 45 +- .../pdo_sqlsrv/pdo_buffered_fetch_types.phpt | 229 ++++ .../pdo_sqlsrv/pdo_connect_encrypted.phpt | 116 +- .../pdo_sqlsrv/pdo_connection_logs.phpt | 65 ++ .../pdo_sqlsrv/pdo_errorMode_logs.phpt | 116 ++ .../pdo_prepare_emulatePrepare_decimal.phpt | 10 +- .../pdo_prepare_emulatePrepare_float.phpt | 10 +- .../pdo_prepare_emulatePrepare_money.phpt | 10 +- .../pdo_sqlsrv/pdo_test_non_LOB_types.phpt | 34 +- .../pdo_sqlsrv/pdostatement_fetchAll.phpt | 10 +- .../pdo_sqlsrv/pdostatement_nextRowset.phpt | 6 +- test/functional/pdo_sqlsrv/skipif_not_hgs.inc | 30 +- test/functional/setup/setup_dbs.py | 5 + test/functional/sqlsrv/53_0021.phpt | 33 +- test/functional/sqlsrv/AEData.inc | 31 +- test/functional/sqlsrv/MsCommon.inc | 19 +- test/functional/sqlsrv/MsSetup.inc | 1 + test/functional/sqlsrv/break.php | 82 +- test/functional/sqlsrv/skipif_not_hgs.inc | 6 +- .../sqlsrv_ae_insert_sqltype_numeric.phpt | 2 +- ...lsrv_ae_output_param_sqltype_datetime.phpt | 243 +++-- ...qlsrv_ae_output_param_sqltype_numeric.phpt | 306 +++--- ...sqlsrv_ae_output_param_sqltype_string.phpt | 256 +++-- .../sqlsrv/sqlsrv_buffered_fetch_types.phpt | 278 +++++ .../functional/sqlsrv/sqlsrv_commit_logs.phpt | 63 ++ test/functional/sqlsrv/sqlsrv_connect.phpt | 2 +- .../sqlsrv/sqlsrv_connect_encrypted.phpt | 111 +- .../sqlsrv/sqlsrv_connect_log_to_file.phpt | 56 + .../sqlsrv/sqlsrv_connect_logs.phpt | 49 + test/functional/sqlsrv/sqlsrv_get_field.phpt | 4 +- .../sqlsrv/srv_007_login_timeout.phpt | 11 +- ...srv_230_sqlsrv_buffered_numeric_types.phpt | 41 +- 77 files changed, 4518 insertions(+), 886 deletions(-) create mode 100644 codecov.yml create mode 100644 test/extended/AE_v2_values.inc create mode 100644 test/extended/MsSetup.inc create mode 100644 test/extended/pdo_AE_functions.inc create mode 100644 test/extended/pdo_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/pdo_aev2_plaintext_string.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_string.phpt create mode 100644 test/extended/skipif_not_hgs.inc create mode 100644 test/extended/sqlsrv_AE_functions.inc create mode 100644 test/extended/sqlsrv_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_plaintext_string.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_connection_logs.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_commit_logs.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_connect_logs.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eab1ff0..0f255250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.8.1 - 2020-04-15 +Updated PECL release packages. Here is the list of updates: + +### Fixed +- Pull Request [#1094](https://github.com/microsoft/msphpsql/pull/1094) - Fixed default locale issues in Alpine Linux +- Pull Request [#1095](https://github.com/microsoft/msphpsql/pull/1095) - Removed unnecessary data structure to support Client-Side Cursors feature in Alpine Linux +- Pull Request [#1095](https://github.com/microsoft/msphpsql/pull/1107) - Fixed logging issues when both drivers are enabled in Alpine Linux + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - Issue [#1050](https://github.com/microsoft/msphpsql/issues/1050) - With Always Encrypted enabled, insertion requires the column list for any tables with identity columns + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) + ## 5.8.0 - 2020-01-31 Updated PECL release packages. Here is the list of updates: diff --git a/Linux-mac-install.md b/Linux-mac-install.md index b3decb93..86f27567 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,7 +1,7 @@ # Linux and macOS Installation Tutorial for the Microsoft Drivers for PHP for SQL Server -The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 19.10, RedHat 7 and 8, Debian 8, 9, and 10, Suse 12 and 15, Alpine 3.11 (experimental), and macOS 10.13, 10.14, and 10.15. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver#loading-the-driver-at-php-startup). +The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 19.10, RedHat 7 and 8, Debian 8, 9, and 10, Suse 12 and 15, Alpine 3.11, and macOS 10.13, 10.14, and 10.15. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver#loading-the-driver-at-php-startup). -These instructions install PHP 7.4 by default. Note that some supported Linux distros default to PHP 7.1 or earlier, which is not supported for the latest version of the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.2 or 7.3 instead. +These instructions install PHP 7.4 by default using `pecl install`. You may need to run `pecl channel-update pecl.php.net` first. Note that some supported Linux distros default to PHP 7.1 or earlier, which is not supported for the latest version of the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.2 or 7.3 instead. Also included are instructions for installing the PHP FastCGI Process Manager, PHP-FPM, on Ubuntu. This is needed if using the nginx web server instead of Apache. @@ -293,13 +293,10 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Alpine 3.11 > [!NOTE] -> Alpine support is experimental. - -> [!NOTE] -> The default version of PHP is 7.3. Alternate versions of PHP are not available from other repositories for Alpine 3.11. You can instead compile PHP from source. +> The default version of PHP is 7.3. Alternate versions of PHP may be available from other repositories for Alpine 3.11. You can instead compile PHP from source. ### Step 1. Install PHP -PHP packages for Alpine are found in the `edge/community` repository. Add the following line to `/etc/apt/repositories`, replacing `` with the URL of an Alpine repository mirror: +PHP packages for Alpine can be found in the `edge/community` repository. Please check [Enable Community Repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) on their WIKI page. Add the following line to `/etc/apt/repositories`, replacing `` with the URL of an Alpine repository mirror: ``` http:///alpine/edge/community ``` @@ -320,10 +317,7 @@ sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/10_pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/00_sqlsrv.ini ``` -You may need to define a locale: -``` -export LC_ALL=C -``` + ### Step 4. Install Apache and configure driver loading ``` sudo apk add php7-apache2 apache2 @@ -391,7 +385,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Testing Your Installation -To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, `/var/www/localhost/htdocs` on Alpine, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. On Alpine 3.11, you may also need to specify the **CharacterSet** as 'UTF-8' in the `$connectionOptions` array. +To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, `/var/www/localhost/htdocs` on Alpine, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. ``` ( dbh->driver_data ); \ - driver_dbh->set_func( __FUNCTION__ ); \ - int length = strlen( __FUNCTION__ ) + strlen( ": entering" ); \ - char func[length+1]; \ - memset(func, '\0', length+1); \ - strcpy_s( func, sizeof( __FUNCTION__ ), __FUNCTION__ ); \ - strcat_s( func, length+1, ": entering" ); \ - LOG( SEV_NOTICE, func ); \ + if (driver_dbh != NULL) driver_dbh->set_func(__FUNCTION__); \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", __FUNCTION__); \ } -#else -#define PDO_LOG_DBH_ENTRY \ -{ \ - pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( dbh->driver_data ); \ - driver_dbh->set_func( __FUNCTION__ ); \ - LOG( SEV_NOTICE, __FUNCTION__ ## ": entering" ); \ -} -#endif // constructor for the internal object for connections pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ) : @@ -547,7 +534,7 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo // 0 for failure, 1 for success. int pdo_sqlsrv_db_handle_factory( _Inout_ pdo_dbh_t *dbh, _In_opt_ zval *driver_options TSRMLS_DC) { - LOG( SEV_NOTICE, "pdo_sqlsrv_db_handle_factory: entering" ); + PDO_LOG_DBH_ENTRY; hash_auto_ptr pdo_conn_options_ht; pdo_error_mode prev_err_mode = dbh->error_mode; diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index 4f1a8845..e471561f 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -128,11 +128,11 @@ PHP_MINIT_FUNCTION(pdo_sqlsrv) ZEND_TSRMLS_CACHE_UPDATE(); #endif - core_sqlsrv_register_logger( pdo_sqlsrv_log ); + core_sqlsrv_register_severity_checker(pdo_severity_check); REGISTER_INI_ENTRIES(); - LOG( SEV_NOTICE, "pdo_sqlsrv: entering minit" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering minit"); // initialize list of pdo errors g_pdo_errors_ht = reinterpret_cast( pemalloc( sizeof( HashTable ), 1 )); @@ -200,7 +200,7 @@ PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv) } catch( ... ) { - LOG( SEV_NOTICE, "Unknown exception caught in PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv)" ); + PDO_LOG_NOTICE("Unknown exception caught in PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv)"); return FAILURE; } @@ -225,18 +225,18 @@ PHP_RINIT_FUNCTION(pdo_sqlsrv) int set_locale = PDO_SQLSRV_G(set_locale_info); if (set_locale == 2) { setlocale(LC_ALL, ""); - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_ALL"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale LC_ALL"); } else if (set_locale == 1) { setlocale(LC_CTYPE, ""); - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_CTYPE"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale LC_CTYPE"); } else { - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale NONE"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale NONE"); } #endif - LOG( SEV_NOTICE, "pdo_sqlsrv: entering rinit" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering rinit"); return SUCCESS; } @@ -250,7 +250,7 @@ PHP_RSHUTDOWN_FUNCTION(pdo_sqlsrv) SQLSRV_UNUSED( module_number ); SQLSRV_UNUSED( type ); - LOG( SEV_NOTICE, "pdo_sqlsrv: entering rshutdown" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering rshutdown"); return SUCCESS; } diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 65f5a7da..37afbf38 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -351,26 +351,13 @@ void stmt_option_fetch_datetime:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op } // log a function entry point -#ifndef _WIN32 #define PDO_LOG_STMT_ENTRY \ { \ pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); \ - driver_stmt->set_func( __FUNCTION__ ); \ - int length = strlen( __FUNCTION__ ) + strlen( ": entering" ); \ - char func[length+1]; \ - memset(func, '\0', length+1); \ - strcpy_s( func, sizeof( __FUNCTION__ ), __FUNCTION__ ); \ - strcat_s( func, length+1, ": entering" ); \ - LOG( SEV_NOTICE, func ); \ + if (driver_stmt != NULL) driver_stmt->set_func( __FUNCTION__ ); \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", __FUNCTION__); \ } -#else -#define PDO_LOG_STMT_ENTRY \ -{ \ - pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); \ - driver_stmt->set_func( __FUNCTION__ ); \ - LOG( SEV_NOTICE, __FUNCTION__ ## ": entering" ); \ -} -#endif // PDO SQLSRV statement destructor pdo_sqlsrv_stmt::~pdo_sqlsrv_stmt( void ) diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 56048330..34e1a4ec 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -39,13 +39,6 @@ const int MAX_DIGITS = 11; // +-2 billion = 10 digits + 1 for the sign if negati // the warning message is not the error message alone; it must take WARNING_TEMPLATE above into consideration without the formats const int WARNING_MIN_LENGTH = static_cast( strlen( WARNING_TEMPLATE ) - strlen( "%1!s!%2!d!%3!s!" )); -// buffer used to hold a formatted log message prior to actually logging it. -const int LOG_MSG_SIZE = 2048; -char log_msg[LOG_MSG_SIZE] = {'\0'}; - -// internal error that says that FormatMessage failed -SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; - // Returns a sqlsrv_error for a given error code. sqlsrv_error_const* get_error_message( _In_opt_ unsigned int sqlsrv_error_code); @@ -623,22 +616,10 @@ void pdo_sqlsrv_retrieve_context_error( _In_ sqlsrv_error const* last_error, _Ou } } -// Formats the error message and writes to the php error log. -void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ) +// check the global variable of pdo_sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool pdo_severity_check(_In_ unsigned int severity TSRMLS_DC) { - if( (severity & PDO_SQLSRV_G( log_severity )) == 0 ) { - return; - } - - DWORD rc = FormatMessage( FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args ); - - // if an error occurs for FormatMessage, we just output an internal error occurred. - if( rc == 0 ) { - SQLSRV_STATIC_ASSERT( sizeof( INTERNAL_FORMAT_ERROR ) < sizeof( log_msg )); - std::copy( INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof( INTERNAL_FORMAT_ERROR ), log_msg ); - } - - php_log_err( log_msg TSRMLS_CC ); + return ((severity & PDO_SQLSRV_G(pdo_log_severity))); } namespace { diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index ab1aaf26..79a294f9 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -29,7 +29,7 @@ // request level variables ZEND_BEGIN_MODULE_GLOBALS(pdo_sqlsrv) -unsigned int log_severity; +unsigned int pdo_log_severity; zend_long client_buffer_max_size; #ifndef _WIN32 diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 2934e11f..2caf1985 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -60,7 +60,7 @@ extern HMODULE g_sqlsrv_hmodule; #endif PHP_INI_BEGIN() - STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, log_severity, + STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, pdo_log_severity, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE , INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, client_buffer_max_size, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) @@ -326,6 +326,10 @@ inline void pdo_reset_dbh_error( _Inout_ pdo_dbh_t* dbh TSRMLS_DC ) } } +#define PDO_LOG_NOTICE(message) \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, message); + #define PDO_RESET_DBH_ERROR pdo_reset_dbh_error( dbh TSRMLS_CC ); inline void pdo_reset_stmt_error( _Inout_ pdo_stmt_t* stmt ) @@ -417,8 +421,8 @@ namespace pdo { } // namespace pdo -// logger for pdo_sqlsrv called by the core layer when it wants to log something with the LOG macro -void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); +// check the global variable of pdo_sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool pdo_severity_check(_In_ unsigned int severity TSRMLS_DC); #endif /* PHP_PDO_SQLSRV_INT_H */ diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index 0b9c8fd9..afa3bf56 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -30,11 +30,6 @@ using namespace core; -// conversion matrix -// each entry holds a function that can perform the conversion or NULL which means the conversion isn't supported -// this is initialized the first time the buffered result set is created. -sqlsrv_buffered_result_set::conv_matrix_t sqlsrv_buffered_result_set::conv_matrix; - namespace { // *** internal types *** @@ -454,34 +449,6 @@ sqlsrv_buffered_result_set::sqlsrv_buffered_result_set( _Inout_ sqlsrv_stmt* stm meta = static_cast( sqlsrv_malloc( col_count * sizeof( sqlsrv_buffered_result_set::meta_data ))); - // set up the conversion matrix if this is the first time we're called - if( conv_matrix.size() == 0 ) { - - conv_matrix[SQL_C_CHAR][SQL_C_CHAR] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_CHAR][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::system_to_wide_string; - conv_matrix[SQL_C_CHAR][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_binary_string; - conv_matrix[SQL_C_CHAR][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::string_to_double; - conv_matrix[SQL_C_CHAR][SQL_C_LONG] = &sqlsrv_buffered_result_set::string_to_long; - conv_matrix[SQL_C_WCHAR][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_WCHAR][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_binary_string; - conv_matrix[SQL_C_WCHAR][SQL_C_CHAR] = &sqlsrv_buffered_result_set::wide_to_system_string; - conv_matrix[SQL_C_WCHAR][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::wstring_to_double; - conv_matrix[SQL_C_WCHAR][SQL_C_LONG] = &sqlsrv_buffered_result_set::wstring_to_long; - conv_matrix[SQL_C_BINARY][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_BINARY][SQL_C_CHAR] = &sqlsrv_buffered_result_set::binary_to_system_string; - conv_matrix[SQL_C_BINARY][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::binary_to_wide_string; - conv_matrix[SQL_C_LONG][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::long_to_double; - conv_matrix[SQL_C_LONG][SQL_C_LONG] = &sqlsrv_buffered_result_set::to_long; - conv_matrix[SQL_C_LONG][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_long; - conv_matrix[SQL_C_LONG][SQL_C_CHAR] = &sqlsrv_buffered_result_set::long_to_system_string; - conv_matrix[SQL_C_LONG][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::long_to_wide_string; - conv_matrix[SQL_C_DOUBLE][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::to_double; - conv_matrix[SQL_C_DOUBLE][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_double; - conv_matrix[SQL_C_DOUBLE][SQL_C_CHAR] = &sqlsrv_buffered_result_set::double_to_system_string; - conv_matrix[SQL_C_DOUBLE][SQL_C_LONG] = &sqlsrv_buffered_result_set::double_to_long; - conv_matrix[SQL_C_DOUBLE][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::double_to_wide_string; - } - SQLSRV_ENCODING encoding = (( stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); @@ -844,18 +811,70 @@ SQLRETURN sqlsrv_buffered_result_set::get_data( _In_ SQLUSMALLINT field_index, _ *out_buffer_length = SQL_NULL_DATA; return SQL_SUCCESS; } - // check to make sure the conversion type is valid - conv_matrix_t::const_iterator conv_iter = conv_matrix.find( meta[field_index].c_type ); - if( conv_iter == conv_matrix.end() || conv_iter->second.find( target_type ) == conv_iter->second.end() ) { - last_error = new (sqlsrv_malloc( sizeof( sqlsrv_error ))) - sqlsrv_error( (SQLCHAR*) "07006", (SQLCHAR*) "Restricted data type attribute violation", 0 ); - return SQL_ERROR; + switch (meta[field_index].c_type) { + case SQL_C_CHAR: + switch (target_type) { + case SQL_C_CHAR: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::system_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_binary_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::string_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::string_to_long(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_WCHAR: + switch (target_type) { + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_binary_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::wide_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::wstring_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::wstring_to_long(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_BINARY: + switch (target_type) { + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::binary_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::binary_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_LONG: + switch (target_type) { + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::long_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::long_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::long_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_DOUBLE: + switch (target_type) { + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::double_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::double_to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::double_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + default: + break; } - return (( this )->*( conv_matrix[meta[field_index].c_type][target_type] ))( field_index, buffer, buffer_length, - out_buffer_length ); + // Should not have reached here, return an error + last_error = new (sqlsrv_malloc(sizeof(sqlsrv_error))) + sqlsrv_error((SQLCHAR*) "07006", (SQLCHAR*) "Restricted data type attribute violation", 0); + return SQL_ERROR; } SQLRETURN sqlsrv_buffered_result_set::get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index ae27400e..e91b44c5 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -287,14 +287,12 @@ struct sqlsrv_static_assert { _In_ static const int value = 1; }; // Logging //********************************************************************************************************************************* // log_callback -// a driver specific callback for logging messages +// a driver specific callback for checking if the messages are qualified to be logged: // severity - severity of the message: notice, warning, or error -// msg - the message to log in a FormatMessage style formatting -// print_args - args to the message -typedef void (*log_callback)( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, _In_opt_ va_list* print_args ); +typedef bool (*severity_callback)(_In_ unsigned int severity TSRMLS_DC); -// each driver must register a log callback. This should be the first thing a driver does. -void core_sqlsrv_register_logger( _In_ log_callback ); +// each driver must register a severity checker callback for logging to work according to the INI settings +void core_sqlsrv_register_severity_checker(_In_ severity_callback driver_checker); // a simple wrapper around a PHP error logging function. void write_to_log( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, ... ); @@ -1746,13 +1744,6 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { sqlsrv_malloc_auto_ptr temp_string; // temp buffer to hold a converted field while in use SQLLEN temp_length; // number of bytes in the temp conversion buffer - typedef SQLRETURN (sqlsrv_buffered_result_set::*conv_fn)( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, - _Inout_ SQLLEN* out_buffer_length ); - typedef std::map< SQLINTEGER, std::map< SQLINTEGER, conv_fn > > conv_matrix_t; - - // two dimentional sparse matrix that holds the [from][to] functions that do conversions - static conv_matrix_t conv_matrix; - // string conversion functions SQLRETURN binary_to_wide_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length ); diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 32b10ad5..7fac8e5b 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -1799,11 +1799,11 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i case SQLSRV_PHPTYPE_INT: { - sqlsrv_malloc_auto_ptr field_value_temp; - field_value_temp = static_cast( sqlsrv_malloc( sizeof( long ))); + sqlsrv_malloc_auto_ptr field_value_temp; + field_value_temp = static_cast( sqlsrv_malloc( sizeof( SQLLEN ))); *field_value_temp = 0; - SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_LONG, field_value_temp, sizeof( long ), + SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_LONG, field_value_temp, sizeof( SQLLEN ), field_len, true /*handle_warning*/ TSRMLS_CC ); CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 2853a22a..bc51b34b 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -23,10 +23,16 @@ namespace { +severity_callback g_driver_severity; + // *** internal constants *** -log_callback g_driver_log; + +// buffer used to hold a formatted log message prior to actually logging it. +const int LOG_MSG_SIZE = 2048; + // internal error that says that FormatMessage failed SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; + // buffer used to hold a formatted log message prior to actually logging it. char last_err_msg[2048] = {'\0'}; // 2k to hold the error messages @@ -35,6 +41,25 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, _In_ unsigned int utf16_len, bool use_strict_conversion = false ); + +// invoked by write_to_log() when the message severity qualifies to be logged +// msg - the message to log in a FormatMessage style formatting +// print_args - args to the message +void log_activity(_In_opt_ const char* msg, _In_opt_ va_list* print_args) +{ + char log_msg[LOG_MSG_SIZE] = { '\0' }; + + DWORD rc = FormatMessage(FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args); + + // if an error occurs for FormatMessage, we just output an internal error occurred. + if (rc == 0) { + SQLSRV_STATIC_ASSERT(sizeof(INTERNAL_FORMAT_ERROR) < sizeof(log_msg)); + std::copy(INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof(INTERNAL_FORMAT_ERROR), log_msg); + } + + php_log_err(log_msg TSRMLS_CC); +} + } // SQLSTATE for all internal errors @@ -47,22 +72,24 @@ SQLCHAR SSPWARN[] = "01SSP"; // the script (sqlsrv_configure). void write_to_log( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, ...) { - SQLSRV_ASSERT( !(g_driver_log == NULL), "Must register a driver log function." ); + SQLSRV_ASSERT( !(g_driver_severity == NULL), "Must register a driver checker function." ); + if (!g_driver_severity(severity TSRMLS_CC)) { + return; + } va_list args; va_start( args, msg ); - g_driver_log( severity TSRMLS_CC, msg, &args ); + log_activity(msg, &args); va_end( args ); } -void core_sqlsrv_register_logger( _In_ log_callback driver_logger ) +void core_sqlsrv_register_severity_checker(_In_ severity_callback driver_checker) { - g_driver_log = driver_logger; + g_driver_severity = driver_checker; } - // convert a string from utf-16 to the encoding and return the new string in the pointer parameter and new // length in the len parameter. If no errors occurred during convertion, true is returned and the original // utf-16 string is released by this function if no errors occurred. Otherwise the parameters are not changed diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index fae23e2f..ec339d8c 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -41,6 +41,9 @@ #define CP_UTF16 1200 #define CP_ACP 0 // default to ANSI code page +bool _setLocale(const char * localeName, std::locale ** pLocale); +void setDefaultLocale(const char ** localeName, std::locale ** pLocale); + // This class provides allocation policies for the SystemLocale and AutoArray classes. // This is primarily needed for the self-allocating ToUtf16/FromUtf16 methods. // SNI needs all its allocations to use its own allocator so it would create a separate diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index 6a69aaa8..22cc8bf6 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -50,38 +50,44 @@ struct cp_iconv // CodePage 2 corresponds to binary. If the attribute PDO::SQLSRV_ENCODING_BINARY // is set, GetIndex() above hits the assert(false) directive unless we include // CodePage 2 below and assign an empty string to it. +#ifdef __MUSL__ +#define TRANSLIT "" +#else +#define TRANSLIT "//TRANSLIT" +#endif + const cp_iconv cp_iconv::g_cp_iconv[] = { { 65001, "UTF-8" }, { 1200, "UTF-16LE" }, { 3, "UTF-8" }, { 2, "" }, - { 1252, "CP1252//TRANSLIT" }, - { 850, "CP850//TRANSLIT" }, - { 437, "CP437//TRANSLIT" }, - { 874, "CP874//TRANSLIT" }, - { 932, "CP932//TRANSLIT" }, - { 936, "CP936//TRANSLIT" }, - { 949, "CP949//TRANSLIT" }, - { 950, "CP950//TRANSLIT" }, - { 1250, "CP1250//TRANSLIT" }, - { 1251, "CP1251//TRANSLIT" }, - { 1253, "CP1253//TRANSLIT" }, - { 1254, "CP1254//TRANSLIT" }, - { 1255, "CP1255//TRANSLIT" }, - { 1256, "CP1256//TRANSLIT" }, - { 1257, "CP1257//TRANSLIT" }, - { 1258, "CP1258//TRANSLIT" }, - { CP_ISO8859_1, "ISO8859-1//TRANSLIT" }, - { CP_ISO8859_2, "ISO8859-2//TRANSLIT" }, - { CP_ISO8859_3, "ISO8859-3//TRANSLIT" }, - { CP_ISO8859_4, "ISO8859-4//TRANSLIT" }, - { CP_ISO8859_5, "ISO8859-5//TRANSLIT" }, - { CP_ISO8859_6, "ISO8859-6//TRANSLIT" }, - { CP_ISO8859_7, "ISO8859-7//TRANSLIT" }, - { CP_ISO8859_8, "ISO8859-8//TRANSLIT" }, - { CP_ISO8859_9, "ISO8859-9//TRANSLIT" }, - { CP_ISO8859_13, "ISO8859-13//TRANSLIT" }, - { CP_ISO8859_15, "ISO8859-15//TRANSLIT" }, + { 1252, "CP1252" TRANSLIT }, + { 850, "CP850" TRANSLIT }, + { 437, "CP437" TRANSLIT }, + { 874, "CP874" TRANSLIT }, + { 932, "CP932" TRANSLIT }, + { 936, "CP936" TRANSLIT }, + { 949, "CP949" TRANSLIT }, + { 950, "CP950" TRANSLIT }, + { 1250, "CP1250" TRANSLIT }, + { 1251, "CP1251" TRANSLIT }, + { 1253, "CP1253" TRANSLIT }, + { 1254, "CP1254" TRANSLIT }, + { 1255, "CP1255" TRANSLIT }, + { 1256, "CP1256" TRANSLIT }, + { 1257, "CP1257" TRANSLIT }, + { 1258, "CP1258" TRANSLIT }, + { CP_ISO8859_1, "ISO8859-1" TRANSLIT }, + { CP_ISO8859_2, "ISO8859-2" TRANSLIT }, + { CP_ISO8859_3, "ISO8859-3" TRANSLIT }, + { CP_ISO8859_4, "ISO8859-4" TRANSLIT }, + { CP_ISO8859_5, "ISO8859-5" TRANSLIT }, + { CP_ISO8859_6, "ISO8859-6" TRANSLIT }, + { CP_ISO8859_7, "ISO8859-7" TRANSLIT }, + { CP_ISO8859_8, "ISO8859-8" TRANSLIT }, + { CP_ISO8859_9, "ISO8859-9" TRANSLIT }, + { CP_ISO8859_13, "ISO8859-13" TRANSLIT }, + { CP_ISO8859_15, "ISO8859-15" TRANSLIT }, { 12000, "UTF-32LE" } }; const size_t cp_iconv::g_cp_iconv_count = ARRAYSIZE(cp_iconv::g_cp_iconv); @@ -279,22 +285,46 @@ bool EncodingConverter::Initialize() using namespace std; +#ifndef _countof + #define _countof(obj) (sizeof(obj)/sizeof(obj[0])) +#endif + +const char* DEFAULT_LOCALES[] = {"en_US.UTF-8", "C"}; + +bool _setLocale(const char * localeName, std::locale ** pLocale) +{ + try + { + *pLocale = new std::locale(localeName); + } + catch(const std::exception& e) + { + return false; + } + + return true; +} + +void setDefaultLocale(const char ** localeName, std::locale ** pLocale) +{ + if(!localeName || !_setLocale(*localeName, pLocale)) + { + int count = 0; + while(!_setLocale(DEFAULT_LOCALES[count], pLocale) && count < _countof(DEFAULT_LOCALES)) + { + count++; + } + + if(localeName) + *localeName = count < _countof(DEFAULT_LOCALES)?DEFAULT_LOCALES[count]:NULL; + } +} + SystemLocale::SystemLocale( const char * localeName ) : m_uAnsiCP(CP_UTF8) , m_pLocale(NULL) { - const char* DEFAULT_LOCALE = "en_US.UTF-8"; - - try { - m_pLocale = new std::locale(localeName); - } - catch(const std::exception& e) { - localeName = DEFAULT_LOCALE; - } - - if(!m_pLocale) { - m_pLocale = new std::locale(localeName); - } + setDefaultLocale(&localeName, &m_pLocale); // Mapping from locale charset to codepage struct LocaleCP diff --git a/source/shared/version.h b/source/shared/version.h index 4369a3c8..40ae57f6 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -27,7 +27,7 @@ // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 #define SQLVERSION_MINOR 8 -#define SQLVERSION_PATCH 0 +#define SQLVERSION_PATCH 1 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index aa460ef9..240c09cf 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -44,13 +44,13 @@ if test "$PHP_SQLSRV" != "no"; then pdo_sqlsrv_inc_path=$srcdir/ext/pdo_sqlsrv/shared/ shared_src_class="" elif test -f $srcdir/ext/sqlsrv/shared/core_sqlsrv.h; then - sqlsrv_inc_path=$srcdir/ext/sqlsrv/shared/ + sqlsrv_inc_path=$srcdir/ext/sqlsrv/shared/ elif test -f $srcdir/shared/core_sqlsrv.h; then - sqlsrv_inc_path=$srcdir/shared/ + sqlsrv_inc_path=$srcdir/shared/ else - AC_MSG_ERROR([Cannot find SQLSRV headers]) + AC_MSG_ERROR([Cannot find SQLSRV headers]) fi - AC_MSG_RESULT($sqlsrv_inc_path) + AC_MSG_RESULT($sqlsrv_inc_path) CXXFLAGS="$CXXFLAGS -std=c++11" CXXFLAGS="$CXXFLAGS -D_FORTIFY_SOURCE=2 -O2" @@ -58,11 +58,17 @@ if test "$PHP_SQLSRV" != "no"; then HOST_OS_ARCH=`uname` if test "${HOST_OS_ARCH}" = "Darwin"; then - SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-bind_at_load" - MACOSX_DEPLOYMENT_TARGET=`sw_vers -productVersion` + SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-bind_at_load" + MACOSX_DEPLOYMENT_TARGET=`sw_vers -productVersion` else - SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-z,now" + SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-z,now" + IS_ALPINE_1=`uname -a | cut -f 4 -d ' ' | cut -f 2 -d '-'` + IS_ALPINE_2=`cat /etc/os-release | grep ID | grep alpine | cut -f 2 -d '='` + if test "${IS_ALPINE_1}" = "Alpine" || test "${IS_ALPINE_2}" = "alpine"; then + AC_DEFINE(__MUSL__, 1, [ ]) + fi fi + PHP_REQUIRE_CXX() PHP_ADD_LIBRARY(stdc++, 1, SQLSRV_SHARED_LIBADD) diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index c4ed6362..58aaf14b 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -629,10 +629,9 @@ const connection_option SS_CONN_OPTS[] = { PHP_FUNCTION ( sqlsrv_connect ) { - LOG_FUNCTION( "sqlsrv_connect" ); - SET_FUNCTION_NAME( *g_ss_henv_cp ); - SET_FUNCTION_NAME( *g_ss_henv_ncp ); + g_ss_henv_cp->set_func(_FN_); + g_ss_henv_ncp->set_func(_FN_); reset_errors( TSRMLS_C ); @@ -785,7 +784,7 @@ PHP_FUNCTION( sqlsrv_close ) // dummy context to pass to the error handler error_ctx = new (sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); if( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &conn_r) == FAILURE ) { @@ -816,7 +815,7 @@ PHP_FUNCTION( sqlsrv_close ) throw ss::SSException(); } - SET_FUNCTION_NAME( *conn ); + conn->set_func(_FN_); // cause any variables still holding a reference to this to be invalid so they cause // an error when passed to a sqlsrv function. There's nothing we can do if the @@ -845,13 +844,15 @@ PHP_FUNCTION( sqlsrv_close ) void __cdecl sqlsrv_conn_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ) { - LOG_FUNCTION( "sqlsrv_conn_dtor" ); + // Without sqlsrv_close(), this function is invoked by php during the final clean up stage. + // To prevent memory/resource leaks, no more logging at this point. + //LOG_FUNCTION( "sqlsrv_conn_dtor" ); // get the structure ss_sqlsrv_conn *conn = static_cast( rsrc->ptr ); SQLSRV_ASSERT( conn != NULL, "sqlsrv_conn_dtor: connection was null"); - SET_FUNCTION_NAME( *conn ); + conn->set_func(__func__); // close all statements associated with the connection. sqlsrv_conn_close_stmts( conn TSRMLS_CC ); diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index bc069205..96f5338f 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -271,8 +271,8 @@ PHP_MINIT_FUNCTION(sqlsrv) { SQLSRV_UNUSED( type ); - core_sqlsrv_register_logger( ss_sqlsrv_log ); - + core_sqlsrv_register_severity_checker(ss_severity_check); + // our global variables are initialized in the RINIT function #if defined(ZTS) if( ts_allocate_id( &sqlsrv_globals_id, diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index 9ca59d9e..fae7082e 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -313,15 +313,11 @@ public: #define LOG_FUNCTION( function_name ) \ const char* _FN_ = function_name; \ SQLSRV_G( current_subsystem ) = current_log_subsystem; \ - LOG( SEV_NOTICE, "%1!s!: entering", _FN_ ); + core_sqlsrv_register_severity_checker(ss_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", _FN_); -#define SET_FUNCTION_NAME( context ) \ -{ \ - (context).set_func( _FN_ ); \ -} - -// logger for ss_sqlsrv called by the core layer when it wants to log something with the LOG macro -void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); +// check the global variables of sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool ss_severity_check(_In_ unsigned int severity TSRMLS_DC); // subsystems that may report log messages. These may be used to filter which systems write to the log to prevent noise. enum logging_subsystems { diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 05d46b6c..c7f002dc 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -1371,7 +1371,6 @@ void __cdecl sqlsrv_stmt_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ) PHP_FUNCTION( sqlsrv_free_stmt ) { - LOG_FUNCTION( "sqlsrv_free_stmt" ); zval* stmt_r = NULL; @@ -1384,7 +1383,7 @@ PHP_FUNCTION( sqlsrv_free_stmt ) // dummy context to pass to the error handler error_ctx = new (sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); // take only the statement resource if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "r", &stmt_r ) == FAILURE ) { diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index 671f6bf4..b91f17c2 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -30,13 +30,6 @@ namespace { // current subsytem. defined for the CHECK_SQL_{ERROR|WARNING} macros unsigned int current_log_subsystem = LOG_UTIL; -// buffer used to hold a formatted log message prior to actually logging it. -const int LOG_MSG_SIZE = 2048; -char log_msg[LOG_MSG_SIZE] = {'\0'}; - -// internal error that says that FormatMessage failed -SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; - // *** internal functions *** sqlsrv_error_const* get_error_message( _In_ unsigned int sqlsrv_error_code ); @@ -457,21 +450,10 @@ ss_error SS_ERRORS[] = { { UINT_MAX, {} } }; -// Formats an error message and finally writes it to the php log. -void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ) +// check the global variables of sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool ss_severity_check(_In_ unsigned int severity TSRMLS_DC) { - if(( severity & SQLSRV_G( log_severity )) && ( SQLSRV_G( current_subsystem ) & SQLSRV_G( log_subsystems ))) { - - DWORD rc = FormatMessage( FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args ); - - // if an error occurs for FormatMessage, we just output an internal error occurred. - if( rc == 0 ) { - SQLSRV_STATIC_ASSERT( sizeof( INTERNAL_FORMAT_ERROR ) < sizeof( log_msg )); - std::copy( INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof( INTERNAL_FORMAT_ERROR ), log_msg ); - } - - php_log_err( log_msg TSRMLS_CC ); - } + return ((severity & SQLSRV_G(log_severity)) && (SQLSRV_G(current_subsystem) & SQLSRV_G(log_subsystems))); } bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_error_code, _In_ bool warning TSRMLS_DC, _In_opt_ va_list* print_args ) @@ -598,7 +580,7 @@ PHP_FUNCTION( sqlsrv_configure ) // dummy context to pass onto the error handler error_ctx = new ( sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); int zr = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "sz", &option, &option_len, &value_z ); CHECK_CUSTOM_ERROR(( zr == FAILURE ), error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { @@ -718,7 +700,7 @@ PHP_FUNCTION( sqlsrv_get_config ) // dummy context to pass onto the error handler error_ctx = new ( sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); int zr = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "s", &option, &option_len ); CHECK_CUSTOM_ERROR(( zr == FAILURE ), error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { diff --git a/test/bvt/sqlsrv/break.inc b/test/bvt/sqlsrv/break.inc index 6a2e190e..5e6429f7 100644 --- a/test/bvt/sqlsrv/break.inc +++ b/test/bvt/sqlsrv/break.inc @@ -1,15 +1,18 @@ "$databaseName", "username"=>"$username", "password"=>"$password" ); $conn = sqlsrv_connect( $serverName, $connectionInfo ); diff --git a/test/bvt/sqlsrv/connect.inc b/test/bvt/sqlsrv/connect.inc index 07f9289a..6ac30a9c 100644 --- a/test/bvt/sqlsrv/connect.inc +++ b/test/bvt/sqlsrv/connect.inc @@ -4,6 +4,8 @@ $databaseName = 'TARGET_DATABASE'; $uid = 'TARGET_USERNAME'; $pwd = 'TARGET_PASSWORD'; +$server2 = 'ANOTHER_SERVER'; + // RevisionNumber in SalesOrderHeader is subject to a trigger incrementing it whenever // changes are made to SalesOrderDetail. Since RevisionNumber is a tinyint, it can // overflow quickly if the BVT tests often run. So we change it directly here first diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt index 9b357732..6e6adfb2 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt @@ -10,8 +10,9 @@ $conn = sqlsrv_connect( $server, $connectionInfo); /* Connect to the local server using Windows Authentication and specify the AdventureWorks database as the database in use. */ -$serverName = "sql-2k14-sp1-1.galaxy.ad"; -$connectionInfo = array( "Database"=>"AdventureWorks2014", "UID"=>"sa", "PWD"=>"Moonshine4me", 'MultipleActiveResultSets'=> false); +$serverName = $server2; +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd, 'MultipleActiveResultSets'=> false); + $conn = sqlsrv_connect( $serverName, $connectionInfo); if( $conn === false ) { diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php b/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php index 751229f1..ea810678 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php +++ b/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php @@ -1,8 +1,8 @@ "AdventureWorks2014", "UID"=>"sa", "PWD"=>"Moonshine4me"); +$serverName = $server2; +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd); $conn = sqlsrv_connect( $serverName, $connectionInfo); if( $conn === false ) { diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt index e9444ea9..a42c9d2f 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt @@ -4,11 +4,8 @@ server side cursor specified when querying --FILE-- "tempdb", "UID"=>"sa", "PWD"=>"Moonshine4me")); - require('connect.inc'); -$connectionInfo = array( "Database"=>"$databaseName", "UID"=>"$uid", "PWD"=>"$pwd"); +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd); $conn = sqlsrv_connect( $server, $connectionInfo); if ( $conn === false ) { die( print_r( sqlsrv_errors(), true )); diff --git a/test/extended/AE_v2_values.inc b/test/extended/AE_v2_values.inc new file mode 100644 index 00000000..d4f32638 --- /dev/null +++ b/test/extended/AE_v2_values.inc @@ -0,0 +1,220 @@ +$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +$targetCeValues = array('correct' =>$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +// Names of the encryption keys, depending on whether we are using Windows +// or AKV authentication (defined in MsSetup.inc). -enclave keys are enclave +// enabled, -noenclave keys are not enclave enabled. +// $targetKeys are the keys used for re-encrypting encrypted columns +if ($keystore == 'win') { + $keys = array("CEK-win-enclave", + "CEK-win-noenclave" + ); + $targetKeys = array("CEK-win-enclave", + "CEK-win-noenclave", + "CEK-win-enclave2", + "CEK-win-noenclave2" + ); +} elseif ($keystore == 'akv') { + $keys = array("CEK-akv-enclave", + "CEK-akv-noenclave" + ); + $targetKeys = array("CEK-akv-enclave", + "CEK-akv-noenclave", + "CEK-akv-enclave2", + "CEK-akv-noenclave2" + ); +} else { + die("No keystore specified! Aborting...\n"); +} + +// $targetTypes are the encryption types used for re-encrypting encrypted columns +$encryptionTypes = array("Deterministic", + "Randomized", + ); +$targetTypes = array("Deterministic", + "Randomized", + ); + + +// Length of the string-type columns. $slength is length as a string instead of integer +$length = 64; +$slength = '64'; + +// Testing the following data types, split into two arrays because if we try one array, +// at some point we get a CE405 error for no clear reason (might be a memory issue?). +// TODO: Follow up and see if we can use a single type array. +$dataTypes1 = array('integer', + 'bigint', + 'smallint', + 'tinyint', + 'bit', + 'float', + 'real', + 'numeric', + 'date', + 'time', + 'datetime', + 'datetime2', + 'datetimeoffset', + 'smalldatetime', + ); + +$dataTypes2 = array('char', + 'nchar', + 'varchar', + 'nvarchar', + 'varchar(max)', + 'nvarchar(max)', + 'binary', + 'varbinary', + 'varbinary(max)', + ); + +// Construct the array of column names. Two columns for each data type, +// one encrypted (suffixed _AE) and one not encrypted. +$colNames1 = array(); +$colNamesAE1 = array(); +$colNames2 = array(); +$colNamesAE2 = array(); + +foreach ($dataTypes1 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames1[$type] = "c_".$column; + $colNamesAE1[$type] = "c_".$column."_AE"; +} + +foreach ($dataTypes2 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames2[$type] = "c_".$column; + $colNamesAE2[$type] = "c_".$column."_AE"; +} + +// The test data below is a mixture of random data and edge cases +$testValues = array(); + +// integers +$testValues['integer'] = array(0,-1,1,2147483647,-2147483648,65536,-100000000,128,9); +$testValues['bigint'] = array(9223372036854775807,-40,0,1,2147483647,-2147483648,65536,-100000000000000); +$testValues['smallint'] = array(4,-4,-32768,-99,32767,-30000,-12,-1); +$testValues['tinyint'] = array(2,0,255,254,99,101,100,32); +$testValues['bit'] = array(1,1,0,0,0,0,1,0); + +// floating point +$testValues['float'] = array(3.14159,2.3e+12,-2.3e+12,2.23e-308,1,-1.79e+308,892.3098234234001,1.2); +$testValues['real'] = array(3.14159,2.3e+12,-2.3e+12,1.18e-38,1,-3.4e+38,892.3098234234001,1.2); +$testValues['numeric'] = array(-3.14159,1.003456789,45.6789,0.0001,987987.12345,-987987.12345,100000000000,-100000000000); + +// dates and times +$testValues['date'] = array('2010-01-31','0485-03-31','7825-07-23','9999-12-31','1956-02-27','2018-09-01','5401-11-02','1031-10-04'); +$testValues['time'] = array('12:40:40','08:14:54.3096','23:59:59.9999','01:00:34.0101','21:45:45.4545','00:23:45.6','17:48:00.0000','20:31:49.0001'); +$testValues['datetime2'] = array('9801-01-29 11:45:23.5092856','2384-12-31 12:40:12.5434323','1984-09-25 10:40:20.0909111','9999-12-31 23:59:59.999999', + '1259-04-29 23:59:59.9999999','1748-09-21 17:48:54.723','3125-05-31 05:00:32.4','0001-01-01 00:00:00'); +$testValues['datetimeoffset'] = array('9801-01-29 11:45:23.5092856-12:45','0001-01-01 00:00:00-02:30','1984-09-25 10:40:20.0909111+03:00','1748-09-21 17:48:54.723-09:21', + '4896-05-18 23:17:58.3-02:00','1657-08-04 18:14:27.4','2022-03-17 07:31:45.890342+09:30','1987-10-25 14:27:34.6320945-06:00'); +$testValues['datetime'] = array('9801-01-29 11:45:23.509','2384-12-31 12:40:12.543','1984-09-25 10:40:20.090','9999-12-31 23:59:59.997', + '2753-04-29 23:59:59.997','1948-09-21 17:48:54.723','3125-05-31 05:00:32.4','2001-01-01 00:00:00'); +$testValues['smalldatetime'] = array('1998-06-13 04:00:00','1985-03-31 12:40:00','2025-07-23 05:00:00','1999-12-31 00:00:00', + '1956-02-27 23:59:00','2018-09-01 14:35:00','2079-06-06 23:59:00','1931-10-04 19:52:00'); + +// strings, ascii and unicode +$testValues['char'] = array('wvyxz', 'tposw', '%c@kj>5', 'fd4$_w@q^@!coe$7', 'abcd', 'ev72#x*fv=u$', '4rfg3sw', 'voi%###i<@@'); +$testValues['nchar'] = array('⽧㘎ⷅ㪋','af㋮ᶄḉㇼ៌ӗඣ','ኁ㵮ഖᅥ㪮ኸ⮊ߒᙵꇕ⯐គꉟफ़⻦ꈔꇼŞ','ꐷꬕ','㐯㩧㖃⺵㴰ڇལᧆ겴ꕕ겑וֹꔄ若㌉ᒵȅ㗉ꗅᡉ','ʭḪぅᾔᎀ㍏겶ꅫၞ㴉ᴳ㜞҂','','בּŬḛʼꃺꌖ㓵ꗛ᧽ഭწ社⾯㬄౧ຸฬ㐯ꋛ㗾'); +$testValues['varchar'] = array('gop093','*#$@@)%*$@!%','cio4*3do*$','zzz$a#l',' ','v#x%n!k&r@p$f^','6$gt?je#~','0x3dK#?'); +$testValues['nvarchar'] = array('ᾁẴ㔮㖖ୱܝ㐗㴴៸ழ᷂ᵄ葉អ㺓節','ӕᏵ൴ꔓὀ⾼','Ὡ','璉Džꖭ갪ụ⺭','Ӿϰᇬ㭡㇑ᵈᔆ⽹hᙎ՞ꦣ㧼ለͭ','Ĕ㬚㔈♠既','ꁈ ݫ','ꍆફⷌ㏽̗ૣܯ¢⽳㌭ゴᔓᅄѓⷉꘊⶮᏏᴗஈԋ≡ㄊହꂈ꓂ꑽრꖾŞ⽉걹ꩰോఫ㒧㒾㑷藍㵀ဲ更ꧥ'); +$testValues['varchar(max)'] = array('Q0H4@4E%v+ 3*Trx#>*r86-&d$VgjZ','AjEvVABur(A&Q@eG,A$3u"xAzl','z#dFd4z', + '9Dvsg9B?7oktB@|OIqy<\K^\e|*7Y&yH31E-<.hQ:)g Jl`MQV>rdOhjG;B4wQ(WR[`l(pELt0FYu._T3+8tns!}Nqrc1%n@|N|ik C@ 03a/ +H9mBq', + 'SSs$Ie*{:D4;S]',' ','<\K^\e|*7Y&yH31E-<.hQ:','@Kg1Z6XTOgbt?CEJ|M^rkR_L4{1?l', '<=', '>=', '<>', '!<', '!>'); + +// Thresholds against which to use the comparison operators +$thresholds = array('integer' => 0, + 'bigint' => 0, + 'smallint' => 1000, + 'tinyint' => 100, + 'bit' => 0, + 'float' => 1.2, + 'real' => -1.2, + 'numeric' => 45.6789, + 'char' => 'rstuv', + 'nchar' => '㊃ᾞਲ㨴꧶ꁚꅍ', + 'varchar' => '6$gt?je#~', + 'nvarchar' => 'ӕᏵ൴ꔓὀ⾼', + 'varchar(max)' => 'hijkl', + 'nvarchar(max)' => 'xᐕᛙᘡ', + 'binary' => '44EE4A', + 'varbinary' => 'E43004FF', + 'varbinary(max)' => 'D3EA762C78FC', + 'date' => '2010-01-31', + 'time' => '21:45:45.4545', + 'datetime' => '3125-05-31 05:00:32.4', + 'datetime2' => '2384-12-31 12:40:12.5434323', + 'datetimeoffset' => '1984-09-25 10:40:20.0909111+03:00', + 'smalldatetime' => '1998-06-13 04:00:00', + ); + +// String patterns to test with LIKE +// For AE, LIKE only works with string types for now. Additional types +// are listed here because eventually the type conversions required for +// pattern matching non-string types should be supported. +$patterns = array('integer' => array('8', '48', '123'), + 'bigint' => array('000','7', '65536'), + 'smallint' => array('4','768','abc'), + 'tinyint' => array('9','0','25'), + 'bit' => array('0','1','100'), + 'float' => array('14159','.','E+','2.3','308'), + 'real' => array('30','.','e-','2.3','38'), + 'numeric' => array('0','0000','12345','abc','.'), + 'char' => array('w','@','x*fv=u$','e3'), + 'nchar' => array('af㋮','㐯ꋛ㗾','ꦣ㧼ለͭ','123'), + 'varchar' => array(' ','a','#','@@)'), + 'nvarchar' => array(' ','Ӿϰᇬ㭡','璉Džꖭ갪ụ⺭','更ꧥ','ꈔꇼŞ'), + 'varchar(max)' => array('A','|*7Y&','4z','@!@','AjE'), + 'nvarchar(max)' => array('t','㧶ᐁቴƯɋ','ᘷ㬡',' ','ꐾɔᡧ㝚'), + 'binary' => array('44EE4A'), + 'varbinary' => array('E43004FF'), + 'varbinary(max)' => array('D3EA762C78FC'), + 'date' => array('20','%','9-','04'), + 'time' => array('4545','.0','20:','12345',':'), + 'datetime' => array('997','12',':5','9999'), + 'datetime2' => array('3125-05-31 05:','.45','$f#','-29 ','0001'), + 'datetimeoffset' => array('+02','96',' ','5092856',':00'), + 'smalldatetime' => array('00','1999','abc',':','06'), + ); +?> diff --git a/test/extended/MsSetup.inc b/test/extended/MsSetup.inc new file mode 100644 index 00000000..36be4308 --- /dev/null +++ b/test/extended/MsSetup.inc @@ -0,0 +1,58 @@ + $database, "UID" => $userName, "PWD" => $userPassword, "TraceOn" => false, "Driver" => $driver); +$daasMode = false; +$marsMode = true; + +$traceEnabled = false; + +$adServer = 'TARGET_AD_SERVER'; +$adDatabase = 'TARGET_AD_DATABASE'; +$adUser = 'TARGET_AD_USERNAME'; +$adPassword = 'TARGET_AD_PASSWORD'; + +if (isset($_ENV['MSSQL_SERVER']) || isset($_ENV['MSSQL_USER']) || isset($_ENV['MSSQL_PASSWORD'])) { + $server = $_ENV['MSSQL_SERVER']; + $uid = $_ENV['MSSQL_USER']; + $pwd = $_ENV['MSSQL_PASSWORD']; + $databaseName = $_ENV['MSSQL_DATABASE_NAME']; +} else { + $uid = $userName; + $pwd = $userPassword; + $databaseName = $database; +} + +// column encryption variables +$keystore = "none"; // key store provider, acceptable values are none, win, ksp, akv +$dataEncrypted = false; // whether data is to be encrypted + +// for Azure Key Vault +$AKVKeyStoreAuthentication = 'TARGET_AKV_AUTH'; // can be KeyVaultPassword or KeyVaultClientSecret +$AKVPrincipalName = 'TARGET_AKV_PRINCIPAL_NAME'; // for use with KeyVaultPassword +$AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPassword +$AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret +$AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret + +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; +?> \ No newline at end of file diff --git a/test/extended/pdo_AE_functions.inc b/test/extended/pdo_AE_functions.inc new file mode 100644 index 00000000..7f52c939 --- /dev/null +++ b/test/extended/pdo_AE_functions.inc @@ -0,0 +1,911 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = ($attestationType == 'disabled') ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = "sqlsrv:Server=$server;Database=$databaseName;ColumnEncryption=$attestationInfo"; + + if ($keystore == 'akv') { + + $securityInfo = ''; + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVPrincipalName"; + $securityInfo .= ";KeyStoreSecret=$AKVPassword"; + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVClientID"; + $securityInfo .= ";KeyStoreSecret=$AKVSecret"; + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options .= $securityInfo; + } + + try { + $conn = new PDO($options, $uid, $pwd); + } catch (PDOException $error) { + $e = $error->errorInfo; + checkErrors($e, array('CE400', '0')); + return false; + } + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + + $stmt = $conn->query($query); + if (!$stmt) { + print_r($conn->errorInfo()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = $stmt->fetch(); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->exec("DBCC FREEPROCCACHE"); + + unset($stmt); + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Insert the data using PDO::prepare() + try { + $stmt = $conn->prepare($insertQuery); + $i=1; + foreach ($dataTypes as $type) { + $PDOType = getPDOType($type); + if (!dataTypeIsBinary($type)) { + $stmt->bindParam($i, $testValues[$type][$v], $PDOType); + $stmt->bindParam($i+1, $testValues[$type][$v], $PDOType); + } else { + // unset() is necessary because otherwise the same data may be + // inserted into multiple binary columns. + unset($val); + $val=pack('H*', $testValues[$type][$v]); + $stmt->bindParam($i, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam($i+1, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + $i+=2; + } + $stmt->execute(); + } catch (PDOException $error) { + if (!$ceDisabled) { + print_r($error); + die("Inserting values in encrypted table failed\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } + } + } + + unset($stmt); +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + try { + $stmt = $conn->query($alterQuery); + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if ($sameKeyAndType) { + print_r($error); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r($error); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } + + unset($stmt); + + return true; +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + try { + $nonAEstmt->execute(); + } catch(Exception $error) { + print_r($error); + die("Executing non-AE computation statement failed!\n"); + } + + try { + $AEstmt->execute(); + } catch(Exception $error) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + $e = $error->errorInfo; + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + return; + } + + $AEres = $AEstmt->fetchAll(PDO::FETCH_NUM); + $nonAEres = $nonAEstmt->fetchAll(PDO::FETCH_NUM); + $AEcount = count($AEres); + $nonAEcount = count($nonAEres); + + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + return; + } elseif ($AEcount > $nonAEcount) { + print_r("Too many AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } elseif ($AEcount < $nonAEcount) { + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } else { + if ($AEcount != 0) { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr[0] != $nonAEres[$i][0]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i][0]." and non-AE result ".$nonAEres[$i][0]."\n"); + } + ++$i; + } + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the Latin1_General_BIN2 + // collation. If the COLLATE clause is left out, we get different + // results between the encrypted and non-encrypted columns (probably + // because the collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $unicode = dataTypeIsUnicode($type); + $PDOType = getPDOType($type); + unset($threshold); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + try { + $AEstmt = $conn->prepare($AEQuery); + $nonAEstmt = $conn->prepare($nonAEQuery); + + if (!dataTypeIsBinary($type)) { + $AEstmt->bindParam(1, $threshold, $PDOType); + $nonAEstmt->bindParam(1, $threshold, $PDOType); + } else { + $AEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $nonAEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + + $patternArray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternArray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $PDOType = getPDOType($type); + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + try { + $AEstmt = $conn->prepare($AEQuery); + $AEstmt->bindParam(1, $spattern, $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $spattern, $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the PDOException ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0] and $code[1]==$errors[1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPDOType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + return PDO::PARAM_INT; + case "bit": + return PDO::PARAM_BOOL; + case "real": + case "float": + case "double": + case "numeric": + case "time": + case "date": + case "datetime2": + case "datetime": + case "datetimeoffset": + case "smalldatetime": + case "money": + case "smallmoney"; + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return PDO::PARAM_STR; + case "binary": + case "varbinary": + case "varbinary(max)": + return PDO::PARAM_LOB; + default: + die("Case is missing for $type type in getPDOType.\n"); + } +} + +?> diff --git a/test/extended/pdo_aev2_plaintext_nonstring.phpt b/test/extended/pdo_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..81e47198 --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_plaintext_string.phpt b/test/extended/pdo_aev2_plaintext_string.phpt new file mode 100644 index 00000000..14bc276f --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..0821e8e3 --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..e016fbfd --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/skipif_not_hgs.inc b/test/extended/skipif_not_hgs.inc new file mode 100644 index 00000000..dd4614de --- /dev/null +++ b/test/extended/skipif_not_hgs.inc @@ -0,0 +1,36 @@ +$uid, "PWD"=>$pwd, "Driver" => $driver); + +$conn = sqlsrv_connect( $server, $connectionInfo ); +if ($conn === false) { + die( "skip Could not connect during SKIPIF." ); +} + +$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; +$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; +$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; + +if ($msodbcsql_maj < 17) { + die("skip Unsupported ODBC driver version"); +} + +if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { + die("skip Unsupported ODBC driver version"); +} + +// Get SQL Server +$server_info = sqlsrv_server_info($conn); +if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { + die("skip Server is not HGS enabled"); +} +?> diff --git a/test/extended/sqlsrv_AE_functions.inc b/test/extended/sqlsrv_AE_functions.inc new file mode 100644 index 00000000..5760b65f --- /dev/null +++ b/test/extended/sqlsrv_AE_functions.inc @@ -0,0 +1,991 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if ($key == $targetKey and $encryptionType == $targetType and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = $attestationType == 'disabled' ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'CharacterSet'=>'UTF-8', + 'ColumnEncryption'=>$attestationInfo, + 'TraceOn'=>true, + 'TraceOn'=>'c:\Users\davidp\Documents\SQL.LOG', + ); + + if ($keystore == 'akv') { + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVPrincipalName, + 'KeyStoreSecret'=>$AKVPassword, + ); + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVClientID, + 'KeyStoreSecret'=>$AKVSecret, + ); + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options = array_merge($options, $securityInfo); + } + + $conn = sqlsrv_connect($server, $options); + if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); + return false; + } + else + { + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + print_r(sqlsrv_errors()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = sqlsrv_fetch_array($stmt); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Enable rich computations + sqlsrv_query($conn, "DBCC traceon(127,-1);"); + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + } + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + global $length; + + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Use pack() on binary data + $params = array(); + foreach ($dataTypes as $type) { + $SQLType = getSQLType($type, $length); + $PHPType = getPHPType($type); + $val = dataTypeIsBinary($type) ? pack('H*', $testValues[$type][$v]) : $testValues[$type][$v]; + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + } + + // Insert the data using sqlsrv_prepare() + $stmt = sqlsrv_prepare($conn, $insertQuery, $params); + if ($stmt == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at prepare\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + if (sqlsrv_execute($stmt) == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at execute\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + sqlsrv_free_stmt($stmt); + } +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if ($sameKeyAndType) { + print_r(sqlsrv_errors()); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r(sqlsrv_errors()); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } else { + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + unset($stmt); + + return true; + } +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + if (!sqlsrv_execute($nonAEstmt)) { + print_r(sqlsrv_errors()); + die("Executing non-AE computation statement failed!\n"); + } + + if(!sqlsrv_execute($AEstmt)) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } else { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + return; + } + + while($AEres = sqlsrv_fetch_array($AEstmt, SQLSRV_FETCH_NUMERIC)) { + $nonAEres = sqlsrv_fetch_array($nonAEstmt, SQLSRV_FETCH_NUMERIC); + if (!$nonAEres) { + print_r($AEres); + print_r(sqlsrv_errors()); + print_r("Too many AE results for operation $comparison and data type $type!\n"); + } else { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr != $nonAEres[$i]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i]." and non-AE result ".$nonAEres[$i]."\n"); + print_r(sqlsrv_errors()); + } + ++$i; + } + } + } + + if ($rr = sqlsrv_fetch_array($nonAEstmt)) { + print_r($rr); + print_r(sqlsrv_errors()); + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the PHPTYPE to be specified to + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $unicode = dataTypeIsUnicode($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = getPHPType($type); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $param = array(array($threshold, SQLSRV_PARAM_IN, $phptype, getSQLType($type, $length))); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + $patternarray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternarray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $slength = $unicode ? iconv_strlen($spattern) : strlen($spattern); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + $sqltype = $unicode ? SQLSRV_SQLTYPE_NCHAR($slength) : SQLSRV_SQLTYPE_CHAR($slength); + + $param = array(array($spattern, SQLSRV_PARAM_IN, $phptype, $sqltype)); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the output of sqlsrv_errors() ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0][0] and $code[1]==$errors[0][1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPHPType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + case "bit": + return SQLSRV_PHPTYPE_INT; + break; + case "real": + case "float": + case "double": + return SQLSRV_PHPTYPE_FLOAT; + break; + case "numeric": + case "money": + case "smallmoney": + case "time": + case "date": + case "datetime": + case "datetime2": + case "datetimeoffset": + case "smalldatetime": + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + return SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR); + break; + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return SQLSRV_PHPTYPE_STRING('UTF-8'); + break; + case "binary": + case "varbinary": + case "varbinary(max)": + return SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY); + break; + default: + die("Case is missing for $type type in GetPHPType.\n"); + } +} + +function getSQLType($type, $length) +{ + switch($type) { + case "bigint": + return SQLSRV_SQLTYPE_BIGINT; + case "integer": + return SQLSRV_SQLTYPE_INT; + case "smallint": + return SQLSRV_SQLTYPE_SMALLINT; + case "tinyint": + return SQLSRV_SQLTYPE_TINYINT; + case "bit": + return SQLSRV_SQLTYPE_BIT; + case "real": + return SQLSRV_SQLTYPE_REAL; + case "float": + case "double": + return SQLSRV_SQLTYPE_FLOAT; + case "numeric": + return SQLSRV_SQLTYPE_NUMERIC(18,0); + case "time": + return SQLSRV_SQLTYPE_TIME; + case "date": + return SQLSRV_SQLTYPE_DATE; + case "datetime": + return SQLSRV_SQLTYPE_DATETIME; + case "datetime2": + return SQLSRV_SQLTYPE_DATETIME2; + case "datetimeoffset": + return SQLSRV_SQLTYPE_DATETIMEOFFSET; + case "smalldatetime": + return SQLSRV_SQLTYPE_SMALLDATETIME; + case "money": + return SQLSRV_SQLTYPE_MONEY; + case "smallmoney": + return SQLSRV_SQLTYPE_SMALLMONEY; + case "xml": + return SQLSRV_SQLTYPE_XML; + case "uniqueidentifier": + return SQLSRV_SQLTYPE_UNIQUEIDENTIFIER; + case "char": + return SQLSRV_SQLTYPE_CHAR($length); + case "varchar": + return SQLSRV_SQLTYPE_VARCHAR($length); + case "varchar(max)": + return SQLSRV_SQLTYPE_VARCHAR('max'); + case "nchar": + return SQLSRV_SQLTYPE_NCHAR($length); + case "nvarchar": + return SQLSRV_SQLTYPE_NVARCHAR($length); + case "nvarchar(max)": + return SQLSRV_SQLTYPE_NVARCHAR('max'); + case "binary": + return SQLSRV_SQLTYPE_BINARY($length); + break; + case "varbinary": + return SQLSRV_SQLTYPE_VARBINARY($length); + break; + case "varbinary(max)": + return SQLSRV_SQLTYPE_VARBINARY('max'); + break; + default: + die("Case is missing for $type type in getSQLType.\n"); + } +} + +?> diff --git a/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..a4f520f9 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_plaintext_string.phpt b/test/extended/sqlsrv_aev2_plaintext_string.phpt new file mode 100644 index 00000000..b258e534 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..5574b425 --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..6d2635eb --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/break_pdo.php b/test/functional/pdo_sqlsrv/break_pdo.php index c5287a33..0bddb36b 100644 --- a/test/functional/pdo_sqlsrv/break_pdo.php +++ b/test/functional/pdo_sqlsrv/break_pdo.php @@ -22,24 +22,24 @@ function generateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) $stmt = $conn->query($sql); // Insert data - $sql = "INSERT INTO $tableName1 VALUES ( ?, ? )"; + $sql = "INSERT INTO $tableName1 VALUES (?, ?)"; for ($t = 100; $t < 116; $t++) { $stmt = $conn->prepare($sql); $ts = substr(sha1($t), 0, 5); - $params = array( $t,$ts ); + $params = array($t, $ts); $stmt->execute($params); } // Create table - $sql = "CREATE TABLE $tableName2 ( c1 INT, c2 VARCHAR(40) )"; + $sql = "CREATE TABLE $tableName2 (c1 INT, c2 VARCHAR(40))"; $stmt = $conn->query($sql); // Insert data - $sql = "INSERT INTO $tableName2 VALUES ( ?, ? )"; + $sql = "INSERT INTO $tableName2 VALUES (?, ?)"; for ($t = 200; $t < 209; $t++) { $stmt = $conn->prepare($sql); $ts = substr(sha1($t), 0, 5); - $params = array( $t,$ts ); + $params = array($t, $ts); $stmt->execute($params); } @@ -52,8 +52,12 @@ function generateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) // Break connection by getting the session ID and killing it. // Note that breaking a connection and testing reconnection requires a // TCP/IP protocol connection (as opposed to a Shared Memory protocol). +// Wait one second before and after breaking to ensure the break occurs +// in the correct order, otherwise there may be timing issues in Linux +// that can cause tests to fail intermittently and unpredictably. function breakConnection($conn, $conn_break) { + sleep(1); $stmt1 = $conn->query("SELECT @@SPID"); $obj = $stmt1->fetch(PDO::FETCH_NUM); $spid = $obj[0]; @@ -69,11 +73,11 @@ function dropTables($server, $uid, $pwd, $tableName1, $tableName2) $conn = new PDO("sqlsrv:server = $server ; Database = $dbName ;", $uid, $pwd); - $query="IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; - $stmt=$conn->query($query); + $query = "IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; + $stmt = $conn->query($query); - $query="IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; - $stmt=$conn->query($query); + $query = "IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; + $stmt = $conn->query($query); } dropTables($server, $uid, $pwd, $tableName1, $tableName2); diff --git a/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt b/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt index 7d5627a2..60273f34 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt @@ -9,6 +9,9 @@ No PDO::PARAM_ tpe specified when binding parameters require_once("MsCommon_mid-refactor.inc"); require_once("AEData.inc"); $dataTypes = array("bit", "tinyint", "smallint", "int", "bigint", "decimal(18,5)", "numeric(10,5)", "float", "real"); + +// Note the size of a float is platform dependent, with a precision of roughly 14 digits +// http://php.net/manual/en/language.types.float.php try { $conn = connect(); foreach ($dataTypes as $dataType) { @@ -26,7 +29,7 @@ try { if ($r === false) { isIncompatibleTypesError($stmt, $dataType, "default type"); } else { - echo "****Encrypted default type is compatible with encrypted $dataType****\n"; + echo "-----Encrypted default type is compatible with encrypted $dataType-----\n"; fetchAll($conn, $tbname); } dropTable($conn, $tbname); @@ -37,49 +40,49 @@ try { echo $e->getMessage(); } ?> ---EXPECT-- +--EXPECTREGEX-- Testing bit: -****Encrypted default type is compatible with encrypted bit**** +-----Encrypted default type is compatible with encrypted bit----- c_det: 1 c_rand: 0 Testing tinyint: -****Encrypted default type is compatible with encrypted tinyint**** +-----Encrypted default type is compatible with encrypted tinyint----- c_det: 0 c_rand: 255 Testing smallint: -****Encrypted default type is compatible with encrypted smallint**** +-----Encrypted default type is compatible with encrypted smallint----- c_det: -32767 c_rand: 32767 Testing int: -****Encrypted default type is compatible with encrypted int**** +-----Encrypted default type is compatible with encrypted int----- c_det: -2147483647 c_rand: 2147483647 Testing bigint: -****Encrypted default type is compatible with encrypted bigint**** +-----Encrypted default type is compatible with encrypted bigint----- c_det: -922337203685479936 c_rand: 922337203685479936 -Testing decimal(18,5): -****Encrypted default type is compatible with encrypted decimal(18,5)**** -c_det: -9223372036854.80000 -c_rand: 9223372036854.80000 +Testing decimal\(18,5\): +-----Encrypted default type is compatible with encrypted decimal\(18,5\)----- +c_det: -9223372036854\.80000 +c_rand: 9223372036854\.80000 -Testing numeric(10,5): -****Encrypted default type is compatible with encrypted numeric(10,5)**** -c_det: -21474.83647 -c_rand: 21474.83647 +Testing numeric\(10,5\): +-----Encrypted default type is compatible with encrypted numeric\(10,5\)----- +c_det: -21474\.83647 +c_rand: 21474\.83647 Testing float: -****Encrypted default type is compatible with encrypted float**** -c_det: -9223372036.8547993 -c_rand: 9223372036.8547993 +-----Encrypted default type is compatible with encrypted float----- +c_det: (-9223372036\.8547993|-9223372036\.8547992) +c_rand: (9223372036\.8547993|9223372036\.8547992) Testing real: -****Encrypted default type is compatible with encrypted real**** -c_det: -2147.4829 -c_rand: 2147.4829 +-----Encrypted default type is compatible with encrypted real----- +c_det: (-2147\.4829|-2147\.483) +c_rand: (2147\.4829|2147\.483) diff --git a/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt new file mode 100644 index 00000000..a10bf6c3 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt @@ -0,0 +1,229 @@ +--TEST-- +Prepare with cursor buffered and fetch a variety of types converted to different types +--DESCRIPTION-- +Test various conversion functionalites for buffered queries with PDO_SQLSRV. +--SKIPIF-- + +--FILE-- +prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + + // Fetch all fields as UTF-8 strings + for ($i = 0; $i < count($inputs); $i++) { + $stmt->execute(); + $f = $stmt->fetchColumn($i); + + if ($f !== $inputs[$i]) { + var_dump($f); + } + } + } catch (PdoException $e) { + echo "Caught exception in fetchAsUTF8:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchArray($conn, $tableName, $inputs) +{ + $query = "SELECT * FROM $tableName"; + try { + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + // By default, even numeric or datetime fields are fetched as strings + $result = $stmt->fetch(PDO::FETCH_NUM); + for ($i = 0; $i < count($inputs); $i++) { + if ($result[$i] !== $inputs[$i]) { + var_dump($f); + } + } + } catch (PdoException $e) { + echo "Caught exception in fetchArray:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchBinaryAsNumber($conn, $tableName, $inputs) +{ + global $violation; + + $query = "SELECT c1 FROM $tableName"; + + try { + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED, PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE=>true)); + $stmt->execute(); + + $stmt->bindColumn('c1', $binaryValue, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + echo "in fetchBinaryAsNumber: exception should have been thrown!\n"; + } catch (PdoException $e) { + // The varbinary field - expect the violation error + if (strpos($e->getMessage(), $violation) === false) { + echo "in fetchBinaryAsNumber: expected '$violation' but caught this:\n"; + echo $e->getMessage() . PHP_EOL; + } + } +} + +function fetchBinaryAsBinary($conn, $tableName, $inputs) +{ + try { + $query = "SELECT c1 FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c1', $binaryValue, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + if ($binaryValue !== $inputs[0]) { + echo "Fetched binary value unexpected: $binaryValue\n"; + } + } catch (PdoException $e) { + echo "Caught exception in fetchBinaryAsBinary:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchFloatAsInt($conn, $tableName) +{ + global $truncation; + + try { + $query = "SELECT c3 FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c3', $floatValue, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + // This should return SQL_SUCCESS_WITH_INFO with the truncation error + $info = $stmt->errorInfo(); + if ($info[0] != '01S07' || $info[2] !== $truncation) { + print_r($stmt->errorInfo()); + } + } catch (PdoException $e) { + echo "Caught exception in fetchFloatAsInt:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchCharAsInt($conn, $tableName, $column) +{ + global $outOfRange; + + try { + $query = "SELECT $column FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn($column, $value, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + // TODO 11297: fix this part outside Windows later + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + echo "in fetchCharAsInt: exception should have been thrown!\n"; + } else { + if ($value != 0) { + var_dump($value); + } + } + } catch (PdoException $e) { + // The (n)varchar field - expect the outOfRange error + if (strpos($e->getMessage(), $outOfRange) === false) { + echo "in fetchCharAsInt ($column): expected '$outOfRange' but caught this:\n"; + echo $e->getMessage() . PHP_EOL; + } + } +} + +function fetchAsNumerics($conn, $tableName, $inputs) +{ + // The following calls expect different errors + fetchFloatAsInt($conn, $tableName); + fetchCharAsInt($conn, $tableName, 'c6'); + fetchCharAsInt($conn, $tableName, 'c7'); + + // The following should work + try { + $query = "SELECT c2, c4 FROM $tableName"; + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c2', $intValue, PDO::PARAM_INT); + $stmt->bindColumn('c4', $decValue, PDO::PARAM_INT); + + $row = $stmt->fetch(PDO::FETCH_BOUND); + + if ($intValue !== intval($inputs[1])) { + var_dump($intValue); + } + if ($decValue !== intval($inputs[3])) { + var_dump($decValue); + } + } catch (PdoException $e) { + echo "Caught exception in fetchAsNumerics:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +try { + $conn = connect(); + $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false); + + $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7'); + $types = array('varbinary(10)', 'int', 'float(53)', 'decimal(16, 6)', 'datetime2', 'varchar(50)', 'nvarchar(50)'); + $inputs = array('abcdefghij', '34567', '9876.5432', '123456789.012340', '2020-02-02 20:20:20.2220000', 'This is a test', 'Şơмė śäოрŀề'); + + // Create table + $colMeta = array(new ColumnMeta($types[0], $columns[0]), + new ColumnMeta($types[1], $columns[1]), + new ColumnMeta($types[2], $columns[2]), + new ColumnMeta($types[3], $columns[3]), + new ColumnMeta($types[4], $columns[4]), + new ColumnMeta($types[5], $columns[5]), + new ColumnMeta($types[6], $columns[6])); + createTable($conn, $tableName, $colMeta); + + // Prepare the input values and insert one row + $query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + if ($i == 0) { + $stmt->bindParam($i+1, $inputs[$i], PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + } else { + $stmt->bindParam($i+1, $inputs[$i]); + } + } + $stmt->execute(); + unset($stmt); + + // Starting fetching using client buffers + fetchAsUTF8($conn, $tableName, $inputs); + fetchArray($conn, $tableName, $inputs); + fetchBinaryAsNumber($conn, $tableName, $inputs); + fetchBinaryAsBinary($conn, $tableName, $inputs); + fetchAsNumerics($conn, $tableName, $inputs); + + // dropTable($conn, $tableName); + echo "Done\n"; + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt b/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt index 124df22b..4ca738b5 100644 --- a/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt +++ b/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt @@ -1,57 +1,63 @@ --TEST-- Test new connection keyword ColumnEncryption +--DESCRIPTION-- +Some test cases return errors as expected. For testing purposes, an enclave enabled +SQL Server and the HGS server are the same instance. If the server is HGS enabled, +the error message of one test case is not the same. --SKIPIF-- --FILE-- getAttribute( PDO::ATTR_CLIENT_VERSION )['DriverVer']; - $msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; -} -catch( PDOException $e ) -{ +try { + $conn = new PDO("sqlsrv:server = $server", $uid, $pwd); + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + $msodbcsqlMaj = $version[0]; + + // Next, check if the server is HGS enabled + $serverInfo = $conn->getAttribute(PDO::ATTR_SERVER_INFO); + if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { + $hgsEnabled = false; + } +} catch (PDOException $e) { echo "Failed to connect\n"; - print_r( $e->getMessage() ); + print_r($e->getMessage()); echo "\n"; } -test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ); +testColumnEncryption($server, $uid, $pwd, $msodbcsqlMaj); echo "Done"; -function verify_output( $PDOerror, $expected ) +function verifyOutput($PDOerror, $expected, $caseNum) { - if( strpos( $PDOerror->getMessage(), $expected ) === false ) - { - print_r( $PDOerror->getMessage() ); + if (strpos($PDOerror->getMessage(), $expected) === false) { + echo "Test case $caseNum failed:\n"; + print_r($PDOerror->getMessage()); echo "\n"; } } -function test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ) +function testColumnEncryption($server, $uid, $pwd, $msodbcsqlMaj) { + global $hgsEnabled; + // Only works for ODBC 17 //////////////////////////////////////// $connectionInfo = "ColumnEncryption = Enabled;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - if($msodbcsql_maj < 17) - { + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + if ($msodbcsqlMaj < 17) { $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; - verify_output( $e, $expected ); - } - else - { - print_r( $e->getMessage() ); + verifyOutput($e, $expected, "1"); + } else { + echo "Test case 1 failed:\n"; + print_r($e->getMessage()); echo "\n"; } } @@ -59,50 +65,42 @@ function test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ) // Works for ODBC 17, ODBC 13 //////////////////////////////////////// $connectionInfo = "ColumnEncryption = Disabled;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - if($msodbcsql_maj < 13) - { + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + if ($msodbcsqlMaj < 13) { $expected = "Invalid connection string attribute"; - verify_output( $e, $expected ); - } - else - { - print_r( $e->getMessage() ); + verifyOutput($e, $expected, "2"); + } else { + echo "Test case 2 failed:\n"; + print_r($e->getMessage()); echo "\n"; } } // should fail for all ODBC drivers + $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; + if ($hgsEnabled) { + $expected = "Requested attestation protocol is invalid."; + } + //////////////////////////////////////// $connectionInfo = "ColumnEncryption = false;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - verify_output( $e, $expected ); + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + verifyOutput($e, $expected, "3"); } // should fail for all ODBC drivers //////////////////////////////////////// $connectionInfo = "ColumnEncryption = 1;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + verifyOutput($e, $expected, "4"); } - catch( PDOException $e ) - { - $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - verify_output( $e, $expected ); - } -} +} ?> --EXPECT-- Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt b/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt new file mode 100644 index 00000000..46ea1cdf --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt @@ -0,0 +1,65 @@ +--TEST-- +Test simple logging with connection, simple query and then close +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + return $conn; +} + +try { + ini_set('log_errors', '1'); + + $logFilename = 'php_errors.log'; + $logFilepath = dirname(__FILE__).'/'.$logFilename; + if (file_exists($logFilepath)) { + unlink($logFilepath); + } + + ini_set('error_log', $logFilepath); + ini_set('pdo_sqlsrv.log_severity', '-1'); + + $conn = toConnect(); + $stmt = $conn->query("SELECT @@Version"); + + // Ignore the fetch results + $stmt->fetchAll(); + + unset($conn); + + if (file_exists($logFilepath)) { + echo file_get_contents($logFilepath); + unlink($logFilepath); + } else { + echo "$logFilepath is missing!\n"; + } + + echo "Done\n"; +} catch (Exception $e) { + var_dump($e); +} + +?> +--EXPECTF-- +[%s UTC] pdo_sqlsrv_db_handle_factory: entering +[%s UTC] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000 +[%s UTC] pdo_sqlsrv_db_handle_factory: error code = 5701 +[%s UTC] pdo_sqlsrv_db_handle_factory: message = %s[SQL Server]Changed database context to '%s'. +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +[%s UTC] pdo_sqlsrv_stmt_describe_col: entering +[%s UTC] pdo_sqlsrv_stmt_fetch: entering +[%s UTC] pdo_sqlsrv_stmt_get_col_data: entering +[%s UTC] pdo_sqlsrv_stmt_fetch: entering +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt b/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt new file mode 100644 index 00000000..a0169696 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt @@ -0,0 +1,116 @@ +--TEST-- +Test different error modes. The queries will try to do a select on a non-existing table +--DESCRIPTION-- +This is similar to pdo_errorMode.phpt but will display the contents of php +error logs based on log severity. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + global $sql; + try { + $q = $conn->query($sql); + } catch (Exception $e) { + // do nothing + } +} + +function testWarning($conn) +{ + // This forces PHP to log errors rather than displaying errors + // on screen -- only required for PDO::ERRMODE_WARNING + ini_set('display_errors', '0'); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + + global $sql; + $q = $conn->query($sql); +} + +function runtests($severity) +{ + global $conn; + + $logFilename = 'php_errors' . $severity . '.log'; + $logFilepath = dirname(__FILE__).'/'.$logFilename; + if (file_exists($logFilepath)) { + unlink($logFilepath); + } + + ini_set('error_log', $logFilepath); + ini_set('pdo_sqlsrv.log_severity', $severity); + + if ($severity === '2' ) { + testWarning($conn); + } else { + testException($conn); + } + + if (file_exists($logFilepath)) { + if ($severity == '0') { + echo "$logFilepath should not exist\n"; + } + echo file_get_contents($logFilepath); + unlink($logFilepath); + } + + // Now reset logging by disabling it + ini_set('pdo_sqlsrv.log_severity', '0'); + echo "Done with $severity\n\n"; +} + +try { + ini_set('log_errors', '1'); + ini_set('pdo_sqlsrv.log_severity', '0'); + + $conn = toConnect(); + $sql = "SELECT * FROM temp_table"; + + runtests('0'); + runtests('1'); + runtests('2'); + runtests('4'); + runtests('-1'); +} catch (Exception $e) { + var_dump($e); +} + +?> +--EXPECTF-- +Done with 0 + +[%s UTC] pdo_sqlsrv_stmt_execute: SQLSTATE = 42S02 +[%s UTC] pdo_sqlsrv_stmt_execute: error code = 208 +[%s UTC] pdo_sqlsrv_stmt_execute: message = %s[SQL Server]Invalid object name 'temp_table'. +Done with 1 + +[%s UTC] PHP Warning: PDO::query(): SQLSTATE[42S02]: Base table or view not found: 208 %s[SQL Server]Invalid object name 'temp_table'. in %spdo_errorMode_logs.php on line %d +Done with 2 + +[%s UTC] pdo_sqlsrv_stmt_dtor: entering +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +Done with 4 + +[%s UTC] pdo_sqlsrv_stmt_dtor: entering +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +[%s UTC] pdo_sqlsrv_stmt_execute: SQLSTATE = 42S02 +[%s UTC] pdo_sqlsrv_stmt_execute: error code = 208 +[%s UTC] pdo_sqlsrv_stmt_execute: message = %s[SQL Server]Invalid object name 'temp_table'. +Done with -1 + diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt index 93899043..d433b911 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt @@ -80,34 +80,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt index eea43902..dee4ff59 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt @@ -79,34 +79,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt index ee4b3699..ae581204 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt @@ -80,34 +80,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt index cd709eb1..37866235 100644 --- a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt +++ b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt @@ -47,6 +47,10 @@ try { verifyResult($result); // test not streamable types + // The size of a float is platform dependent, with a precision of roughly 14 digits + // http://php.net/manual/en/language.types.float.php + // For example, the input value for column [real_type] in setup\test_types.sql is 1.18E-38 + // but in some distros the fetched value is 1.1799999E-38 $tsql = "SELECT * FROM [test_types]"; $stmt = $conn->query($tsql); $result = $stmt->fetch(PDO::FETCH_NUM); @@ -60,19 +64,19 @@ unset($stmt); unset($conn); ?> ---EXPECT-- +--EXPECTREGEX-- Array -( - [0] => 9223372036854775807 - [1] => 2147483647 - [2] => 32767 - [3] => 255 - [4] => 1 - [5] => 9999999999999999999999999999999999999 - [6] => 922337203685477.5807 - [7] => 214748.3647 - [8] => 1.79E+308 - [9] => 1.1799999E-38 - [10] => 1968-12-12 16:20:00.000 - [11] => -) \ No newline at end of file +\( + \[0\] => 9223372036854775807 + \[1\] => 2147483647 + \[2\] => 32767 + \[3\] => 255 + \[4\] => 1 + \[5\] => 9999999999999999999999999999999999999 + \[6\] => 922337203685477\.5807 + \[7\] => 214748\.3647 + \[8\] => 1\.79E\+308 + \[9\] => (1\.18E-38|1\.1799999E-38) + \[10\] => 1968-12-12 16:20:00.000 + \[11\] => +\) \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt index 5f1a4f43..6a30232c 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt @@ -180,9 +180,9 @@ array(2) { [5]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." [7]=> @@ -395,7 +395,7 @@ object(stdClass)#%x (%x) { ["NVarCharCol"]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." } @@ -414,7 +414,7 @@ array(8) { [5]=> string(10) "STRINGCOL2" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [7]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." } @@ -425,7 +425,7 @@ string(10) "STRINGCOL2" string(23) "2000-11-11 11:11:11.223" string(10) "STRINGCOL2" string(10) "STRINGCOL2" -string(18) "222.22200000000001" +string(%d) "222.222%S" string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." Test_9 : FETCH_INVALID : diff --git a/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt b/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt index f235ed94..fa63c225 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt @@ -31,7 +31,7 @@ try { var_dump($e); } ?> ---EXPECT-- +--EXPECTF-- array(1) { [0]=> array(62) { @@ -224,9 +224,9 @@ array(2) { [5]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." [7]=> diff --git a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc index dd4614de..80ad95f0 100644 --- a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc +++ b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc @@ -3,34 +3,34 @@ // SQL Server, and a HGS server. The HGS server and SQL Server // are the same for testing purposes. -if (!extension_loaded("sqlsrv")) { +if (!extension_loaded("pdo_sqlsrv")) { die("skip Extension not loaded"); } -require_once("MsSetup.inc"); +require_once('MsSetup.inc'); -$connectionInfo = array("UID"=>$uid, "PWD"=>$pwd, "Driver" => $driver); - -$conn = sqlsrv_connect( $server, $connectionInfo ); -if ($conn === false) { - die( "skip Could not connect during SKIPIF." ); +$conn = new PDO("sqlsrv:server = $server", $uid, $pwd); +if (!$conn) { + die("skip Could not connect during SKIPIF."); } -$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; -$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; -$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; +$msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)['DriverVer']; +$version = explode(".", $msodbcsqlVer); -if ($msodbcsql_maj < 17) { +$msodbcsqlMaj = $version[0]; +$msodbcsqlMin = $version[1]; + +if ($msodbcsqlMaj < 17) { die("skip Unsupported ODBC driver version"); } -if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { +if ($msodbcsqlMin < 4 and $msodbcsqlMaj == 17) { die("skip Unsupported ODBC driver version"); } // Get SQL Server -$server_info = sqlsrv_server_info($conn); -if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { +$serverInfo = $conn->getAttribute(PDO::ATTR_SERVER_INFO); +if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { die("skip Server is not HGS enabled"); } -?> +?> \ No newline at end of file diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index ead94a30..69844169 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -42,6 +42,8 @@ if __name__ == '__main__': parser.add_argument('-dbname', '--DBNAME', required=True) parser.add_argument('-azure', '--AZURE', required=False, default='no') args = parser.parse_args() + + print("Start\n") try: server = os.environ['TEST_PHP_SQL_SERVER'] @@ -61,10 +63,13 @@ if __name__ == '__main__': if (args.AZURE.lower() == 'no'): manageTestDB('create_db.sql', conn_options, args.DBNAME) + print("About to set up databases...\n") # create tables in the new database setupTestDatabase(conn_options, args.DBNAME, args.AZURE) + print("About to populate tables...\n") # populate these tables populateTables(conn_options, args.DBNAME) + print("About to set up encryption...\n") # setup AE (certificate, column master key and column encryption key) setupAE(conn_options, args.DBNAME) diff --git a/test/functional/sqlsrv/53_0021.phpt b/test/functional/sqlsrv/53_0021.phpt index 8d010299..c9bbf201 100644 --- a/test/functional/sqlsrv/53_0021.phpt +++ b/test/functional/sqlsrv/53_0021.phpt @@ -12,7 +12,12 @@ Test for integer, float, and datetime types vs various sql server types. require( 'MsCommon.inc' ); -function get_fields( $stmt ) { +$epsilon = 0.00001; +$decimals = ['9999999999999999999999999999999999999', '-10000000000000000000000000000000000001', '0']; + +function get_fields( $stmt, $round ) { + + global $epsilon, $decimals; // bigint $field = sqlsrv_get_field( $stmt, 0, SQLSRV_PHPTYPE_INT ); @@ -71,7 +76,22 @@ function get_fields( $stmt ) { } else { var_dump( sqlsrv_errors( SQLSRV_ERR_WARNINGS ) ); - echo "$field\n"; + // The size of a float is platform dependent, with a precision of roughly 14 digits + // http://php.net/manual/en/language.types.float.php + // For example, in Ubuntu 18.04 or macOS Mojave the returned value is 1.0E+37 or -1.0E+37 + // but in Alpine Linux it is 9.9999999999997E+36 or -9.9999999999997E+36 + if ($decimals[$round] == '0') { + if ($field != 0) { + echo "Expected 0 but got $field\n"; + } + } else { + $expected = floatval($decimals[$round]); + $diff = abs(($field - $expected) / $expected); + + if ($diff > $epsilon) { + echo "Expected $expected but got $field -- difference is $diff\n"; + } + } } // datetime @@ -147,7 +167,7 @@ function get_fields( $stmt ) { } // maximum values - get_fields( $stmt ); + get_fields( $stmt, 0 ); $success = sqlsrv_fetch( $stmt ); if( !$success ) { @@ -156,7 +176,7 @@ function get_fields( $stmt ) { } // minimum values - get_fields( $stmt ); + get_fields( $stmt, 1 ); $success = sqlsrv_fetch( $stmt ); if( !$success ) { @@ -165,7 +185,7 @@ function get_fields( $stmt ) { } // zero values - get_fields( $stmt ); + get_fields( $stmt, 2 ); $stmt = sqlsrv_query( $conn, "SELECT int_type, decimal_type, datetime_type, real_type FROM [test_types]" ); if( !$stmt ) { @@ -258,7 +278,6 @@ NULL NULL 1 NULL -1.0E+37 NULL 12/12/1968 04:20:00 NULL @@ -296,7 +315,6 @@ NULL NULL 0 NULL --1.0E+37 NULL 12/12/1968 04:20:00 NULL @@ -319,7 +337,6 @@ NULL NULL 0 NULL -0 NULL 12/12/1968 04:20:00 NULL diff --git a/test/functional/sqlsrv/AEData.inc b/test/functional/sqlsrv/AEData.inc index 3879efce..46dfc6c5 100644 --- a/test/functional/sqlsrv/AEData.inc +++ b/test/functional/sqlsrv/AEData.inc @@ -84,11 +84,9 @@ $sqlTypes = array( function is_incompatible_types_error( $dataType, $sqlType ) { $errors = sqlsrv_errors(); - foreach ( $errors as $error ) - { + foreach ($errors as $error) { // 22018 is the SQLSTATE for the operand crash error for incompatible types - if ( $error['SQLSTATE'] == 22018 ) - { + if ($error['SQLSTATE'] == '22018') { echo "Encrypted $sqlType is incompatible with encrypted $dataType\n"; } } @@ -109,7 +107,6 @@ function get_sqlType_constant( $sqlType ) { switch ( $sqlType ) { case 'SQLSRV_SQLTYPE_BIGINT': - case 'SQLSRV_SQLTYPE_BINARY': case 'SQLSRV_SQLTYPE_BIT': case 'SQLSRV_SQLTYPE_DATE': case 'SQLSRV_SQLTYPE_DATETIME': @@ -135,6 +132,10 @@ function get_sqlType_constant( $sqlType ) case 'SQLSRV_SQLTYPE_XML': return constant( $sqlType ); break; + case 'SQLSRV_SQLTYPE_BINARY': + // our tests always use precision 5 for SQLSRV_SQLTYPE_BINARY + return SQLSRV_SQLTYPE_BINARY(5); + break; case 'SQLSRV_SQLTYPE_CHAR': // our tests always use precision 5 for SQLSRV_SQLTYPE_CHAR return SQLSRV_SQLTYPE_CHAR(5); @@ -146,7 +147,7 @@ function get_sqlType_constant( $sqlType ) case 'SQLSRV_SQLTYPE_NCHAR': // our tests always use precision 5 for SQLSRV_SQLTYPE_NCHAR return SQLSRV_SQLTYPE_NCHAR(5); - break; + break; case 'SQLSRV_SQLTYPE_NUMERIC': // our tests always use precision 10 scale 5 for SQLSRV_SQLTYPE_NUMERIC return SQLSRV_SQLTYPE_NUMERIC(10, 5); @@ -157,7 +158,7 @@ function get_sqlType_constant( $sqlType ) } } -function isDateTimeType( $sqlType ) +function isDateTimeType($sqlType) { return ($sqlType == 'SQLSRV_SQLTYPE_DATE' || $sqlType == 'SQLSRV_SQLTYPE_DATETIME' || @@ -167,4 +168,20 @@ function isDateTimeType( $sqlType ) $sqlType == 'SQLSRV_SQLTYPE_TIME'); } +function isLOBType($sqlType) +{ + return ($sqlType == 'SQLSRV_SQLTYPE_TEXT' || $sqlType == 'SQLSRV_SQLTYPE_NTEXT' || $sqlType == 'SQLSRV_SQLTYPE_IMAGE'); +} + +function isCompatible($compatList, $dataType, $sqlType) +{ + foreach ($compatList[$dataType] as $compatType) { + if (stripos($compatType, $sqlType) !== false) { + return true; + } + } + + return false; +} + ?> diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index dec27c55..195430d5 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -84,6 +84,12 @@ function isDaasMode() return ($daasMode ? true : false); } +function isLocaleDisabled() +{ + global $daasMode, $localeDisabled; + return ($daasMode || $localeDisabled); +} + function isSQLAzure() { // 'SQL Azure' indicates SQL Database or SQL Data Warehouse @@ -491,10 +497,11 @@ function handleErrors() function setUSAnsiLocale() { - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return; } + if (!isWindows()) { // macOS the locale names are different in Linux or macOS $locale = strtoupper(PHP_OS) === 'LINUX' ? "en_US.ISO-8859-1" : "en_US.ISO8859-1"; @@ -505,8 +512,8 @@ function setUSAnsiLocale() function resetLocaleToDefault() { - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return; } // Like setUSAnsiLocale() above, this method is only needed in non-Windows environment @@ -522,8 +529,8 @@ function isLocaleSupported() if (isWindows()) { return true; } - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return false; } if (AE\isDataEncrypted()) { diff --git a/test/functional/sqlsrv/MsSetup.inc b/test/functional/sqlsrv/MsSetup.inc index 8335c13b..f328e17c 100644 --- a/test/functional/sqlsrv/MsSetup.inc +++ b/test/functional/sqlsrv/MsSetup.inc @@ -25,6 +25,7 @@ $daasMode = false; $marsMode = true; $traceEnabled = false; +$localeDisabled = false; $adServer = 'TARGET_AD_SERVER'; $adDatabase = 'TARGET_AD_DATABASE'; diff --git a/test/functional/sqlsrv/break.php b/test/functional/sqlsrv/break.php index 97abcccf..b7bdc43b 100644 --- a/test/functional/sqlsrv/break.php +++ b/test/functional/sqlsrv/break.php @@ -12,79 +12,79 @@ $tableName2 = "test_connres2"; // Using generated tables will eventually allow us to put the // connection resiliency tests on Github, since the integrated testing // from AppVeyor does not have AdventureWorks. -function GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ) +function GenerateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) { - $connectionInfo = array( "Database"=>$dbName, "uid"=>$uid, "pwd"=>$pwd ); + $connectionInfo = array("Database"=>$dbName, "uid"=>$uid, "pwd"=>$pwd); - $conn = sqlsrv_connect( $server, $connectionInfo ); - if ( $conn === false ) - { - die ( print_r( sqlsrv_errors() ) ); + $conn = sqlsrv_connect($server, $connectionInfo); + if ($conn === false) { + die (print_r(sqlsrv_errors())); } // Create table - $sql = "CREATE TABLE $tableName1 ( c1 INT, c2 VARCHAR(40) )"; - $stmt = sqlsrv_query( $conn, $sql ); + $sql = "CREATE TABLE $tableName1 (c1 INT, c2 VARCHAR(40))"; + $stmt = sqlsrv_query($conn, $sql); // Insert data - $sql = "INSERT INTO $tableName1 VALUES ( ?, ? )"; - for( $t = 100; $t < 116; $t++ ) - { - $ts = substr( sha1( $t ),0,5 ); - $params = array( $t,$ts ); - $stmt = sqlsrv_prepare( $conn, $sql, $params ); - sqlsrv_execute( $stmt ); + $sql = "INSERT INTO $tableName1 VALUES (?, ?)"; + for ($t = 100; $t < 116; $t++) { + $ts = substr(sha1($t), 0, 5); + $params = array($t, $ts); + $stmt = sqlsrv_prepare($conn, $sql, $params); + sqlsrv_execute($stmt); } // Create table - $sql = "CREATE TABLE $tableName2 ( c1 INT, c2 VARCHAR(40) )"; - $stmt = sqlsrv_query( $conn, $sql ); + $sql = "CREATE TABLE $tableName2 (c1 INT, c2 VARCHAR(40))"; + $stmt = sqlsrv_query($conn, $sql); // Insert data - $sql = "INSERT INTO $tableName2 VALUES ( ?, ? )"; - for( $t = 200; $t < 209; $t++ ) - { - $ts = substr( sha1( $t ),0,5 ); - $params = array( $t,$ts ); - $stmt = sqlsrv_prepare( $conn, $sql, $params ); - sqlsrv_execute( $stmt ); + $sql = "INSERT INTO $tableName2 VALUES (?, ?)"; + for ($t = 200; $t < 209; $t++) { + $ts = substr(sha1($t), 0, 5); + $params = array($t, $ts); + $stmt = sqlsrv_prepare($conn, $sql, $params); + sqlsrv_execute($stmt); } - sqlsrv_close( $conn ); + sqlsrv_close($conn); } // Break connection by getting the session ID and killing it. // Note that breaking a connection and testing reconnection requires a // TCP/IP protocol connection (as opposed to a Shared Memory protocol). -function BreakConnection( $conn, $conn_break ) +// Wait one second before and after breaking to ensure the break occurs +// in the correct order, otherwise there may be timing issues in Linux +// that can cause tests to fail intermittently and unpredictably. +function BreakConnection($conn, $conn_break) { - $stmt1 = sqlsrv_query( $conn, "SELECT @@SPID" ); - if ( sqlsrv_fetch( $stmt1 ) ) - { - $spid=sqlsrv_get_field( $stmt1, 0 ); + sleep(1); + $stmt1 = sqlsrv_query($conn, "SELECT @@SPID"); + if (sqlsrv_fetch($stmt1)) { + $spid=sqlsrv_get_field($stmt1, 0); } - $stmt2 = sqlsrv_prepare( $conn_break, "KILL ".$spid ); - sqlsrv_execute( $stmt2 ); + $stmt2 = sqlsrv_prepare($conn_break, "KILL ".$spid); + sqlsrv_execute($stmt2); sleep(1); } // Remove the tables generated by GenerateTables -function DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ) +function DropTables($server, $uid, $pwd, $tableName1, $tableName2) { global $dbName; - $connectionInfo = array( "Database"=>$dbName, "UID"=>$uid, "PWD"=>$pwd ); - $conn = sqlsrv_connect( $server, $connectionInfo ); + $connectionInfo = array("Database"=>$dbName, "UID"=>$uid, "PWD"=>$pwd); + $conn = sqlsrv_connect($server, $connectionInfo); - $query="IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; - $stmt=sqlsrv_query( $conn, $query ); + $query = "IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; + $stmt = sqlsrv_query($conn, $query); - $query="IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; - $stmt=sqlsrv_query( $conn, $query ); + $query = "IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; + $stmt = sqlsrv_query($conn, $query); } -DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ); -GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ); +DropTables($server, $uid, $pwd, $tableName1, $tableName2); +GenerateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2); ?> diff --git a/test/functional/sqlsrv/skipif_not_hgs.inc b/test/functional/sqlsrv/skipif_not_hgs.inc index 7d7b3ca1..a25d8cbc 100644 --- a/test/functional/sqlsrv/skipif_not_hgs.inc +++ b/test/functional/sqlsrv/skipif_not_hgs.inc @@ -9,11 +9,11 @@ if (!extension_loaded("sqlsrv")) { require_once("MsSetup.inc"); -$connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword, "Driver" => $driver); +$connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword); -$conn = sqlsrv_connect( $server, $connectionInfo ); +$conn = sqlsrv_connect($server, $connectionInfo); if ($conn === false) { - die( "skip Could not connect during SKIPIF." ); + die("skip Could not connect during SKIPIF."); } $msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt index cb7aaf66..6fd5c3be 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt @@ -52,7 +52,7 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == '22018') { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt index 67c601b8..020dce75 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt @@ -1,111 +1,154 @@ --TEST-- Test for inserting and retrieving encrypted data of datetime types --DESCRIPTION-- -Bind output params using sqlsrv_prepare with all sql_type +Bind output/inout params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetime2" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "smalldatetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "time" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetimeoffset" => array("SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIMEOFFSET") ); - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - + array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetime2" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "smalldatetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "time" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetimeoffset" => array("SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIMEOFFSET") ); + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; + + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array( &$c_detOut, $direction, null, $sqlTypeConstant), + array(&$c_randOut, $direction, null, $sqlTypeConstant )) + ); + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + // With data encrypted, errors are totally expected + if (empty($errors)) { + echo "Encrypted data: $dataType should NOT be compatible with $sqlType\n"; + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible) { + if (!empty($errors)) { + echo "$dataType should be compatible with $sqlType.\n"; + } else { + $success = true; + } + } else { + $implicitConv = 'Implicit conversion from data type '; + + // 22018 is the SQLSTATE for any incompatible conversion errors + if ($errors[0]['SQLSTATE'] === '22018') { + $success = true; + } elseif (strpos($errors[0]['message'], $implicitConv) !== false) { + $success = true; + } else { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +$conn = AE\connect(); + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + // insert a row + // Take the second and third entres (some edge cases) from the various + // $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'date', use $date_params[1] and $date_params[2] + // to form an array, namely ["0001-01-01", "9999-12-31"] $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - $c_detOut = ''; - $c_randOut = ''; - $stmt = sqlsrv_prepare( $conn, $outSql, - array(array( &$c_detOut, SQLSRV_PARAM_OUT, null, $sqlTypeConstant), - array(&$c_randOut, SQLSRV_PARAM_OUT, null, $sqlTypeConstant ))); - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - if (empty($errors) && AE\IsDataEncrypted()) { - // SQLSRV_PHPTYPE_DATETIME not supported - echo "$dataType should not be compatible with any datetime type.\n"; - $success = false; - } - } - } - } - } - - // cleanup - sqlsrv_free_stmt($stmt); - sqlsrv_query($conn, "TRUNCATE TABLE $tbname"); - - if ($success) { - echo "Test successfully done.\n"; - } - - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $r; + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_* constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + // cleanup + sqlsrv_free_stmt($stmt); + sqlsrv_query($conn, "TRUNCATE TABLE $tbname"); + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt index ea5285d8..1fd65641 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt @@ -5,143 +5,181 @@ Bind output params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "tinyint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "smallint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "int" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "bigint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP" ), - "decimal(18,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "numeric(10,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "float" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT"), - "real" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT")); -$epsilon = 0.0001; - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - // TODO: It's a good idea to test conversions between different datatypes when AE is off as well. - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - +$directions = array(SQLSRV_PARAM_OUT, SQLSRV_PARAM_INOUT); + +// this is a list of implicit datatype conversion that SQL Server allows (https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-type-conversion-database-engine) +$compatList = array("bit" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "tinyint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "smallint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "int" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "bigint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP" ), + "decimal(18,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "numeric(10,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "float" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT"), + "real" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT")); + +function compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues) +{ + $epsilon = 0.0001; + $success = true; + + if ($dataType == "float" || $dataType == "real") { + if (abs($c_detOut - $inputValues[0]) > $epsilon || abs($c_randOut - $inputValues[1]) > $epsilon) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + $success = false; + } + } else { + if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + $success = false; + } + } + + return $success; +} + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; + + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array( &$c_detOut, $direction, null, $sqlTypeConstant), + array(&$c_randOut, $direction, null, $sqlTypeConstant )) + ); + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + if (empty($errors)) { + // With data encrypted, it's a lot stricter, so the results are expected + // to be numeric and comparable + $success = compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues); + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible && empty($errors)) { + $success = true; + } else { + // Even if $dataType is compatible with $sqlType sometimes + // we still get errors from the server -- if so, it might + // return either SQLSTATE '42000' or '22018' (operand type + // clash but only happens with some certain types) + // E.g. when converting a bigint to int or an int to numeric, + // SQLSTATE '42000' is returned, indicating an error when + // converting from one type to another. + // TODO 11559: investigate if SQLSTATE '42000' is indeed acceptable + $success = ($errors[0]['SQLSTATE'] === '42000' || ($errors[0]['SQLSTATE'] === '22018' && in_array($sqlType, ['SQLSRV_SQLTYPE_XML', 'SQLSRV_SQLTYPE_BINARY', 'SQLSRV_SQLTYPE_VARBINARY', 'SQLSRV_SQLTYPE_UNIQUEIDENTIFIER', 'SQLSRV_SQLTYPE_TIMESTAMP']))); + if (!$success) { + if ($compatible) { + echo "$dataType should be compatible with $sqlType.\n"; + } else { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + } + + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +$conn = AE\connect(); + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + // insert a row + // Take the second and third entres (some edge cases) from the various + // $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'decimal(18,5)', use $decimal_params[1] and $decimal_params[2] + // to form an array, namely [-9223372036854.80000, 9223372036854.80000] $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - // convert input values to strings for decimals and numerics - if ($dataTypes == "decimal(18,5)" || $dataTypes == "numeric(10,5)") { - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => (string) $inputValues[0], $colMetaArr[1]->colName => (string) $inputValues[1] ), $r); - } else { - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - } - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - if ($sqlType == 'SQLSRV_SQLTYPE_FLOAT' || $sqlType == 'SQLSRV_SQLTYPE_REAL') { - $c_detOut = 0.0; - $c_randOut = 0.0; - } else { - $c_detOut = 0; - $c_randOut = 0; - } - $stmt = sqlsrv_prepare($conn, $outSql, - array(array( &$c_detOut, constant($direction), null, $sqlTypeConstant), - array(&$c_randOut, constant($direction), null, $sqlTypeConstant))); - - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - - if (!empty($errors)) { - if (stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - var_dump(sqlsrv_errors()); - $success = false; - } - } - else { - if (AE\IsDataEncrypted() || stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - if ($dataType == "float" || $dataType == "real") { - if (abs($c_detOut - $inputValues[0]) > $epsilon || abs($c_randOut - $inputValues[1]) > $epsilon) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } else { - if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } - } - } - - sqlsrv_free_stmt($stmt); - } - } - } - } - - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - - if ($success) { - echo "Test successfully done.\n"; - } - - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $r; + // convert input values to strings for decimals and numerics + if ($dataTypes == "decimal(18,5)" || $dataTypes == "numeric(10,5)") { + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => (string) $inputValues[0], $colMetaArr[1]->colName => (string) $inputValues[1] ), $r); + } else { + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + } + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_ constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt index 8bbe9606..65919143 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt @@ -5,117 +5,155 @@ Bind output params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "varchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "nchar(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "nvarchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML")); - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - // TODO: It's a good idea to test conversions between different datatypes when AE is off as well. - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - - // insert a row - $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - $c_detOut = ''; - $c_randOut = ''; - $stmt = sqlsrv_prepare($conn, $outSql, - array(array(&$c_detOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant), - array(&$c_randOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant))); - - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - - if (!empty($errors) ) { - if (stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - var_dump(sqlsrv_errors()); - $success = false; - } - } - else - { - if (AE\IsDataEncrypted() || stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } - } - - sqlsrv_free_stmt($stmt); - } - } - } - } +$directions = array(SQLSRV_PARAM_OUT, SQLSRV_PARAM_INOUT); + +// this is a list of implicit datatype conversion that SQL Server allows (https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-type-conversion-database-engine) +$compatList = array("char(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "varchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "nchar(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "nvarchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML")); + +$conn = AE\connect(); + +function compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues) +{ + $success = true; + if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + + $success = false; + } + + return $success; +} + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - if ($success) { - echo "Test successfully done.\n"; - } - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array(&$c_detOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant), + array(&$c_randOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant)) + ); + + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + if (empty($errors)) { + // With data encrypted, it's a lot stricter, so the results are expected + // to be comparable + $success = compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues); + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible && empty($errors)) { + $success = true; + } else { + // Even if $dataType is compatible with $sqlType sometimes + // we still get errors from the server -- if so, it should + // return SQLSTATE '42000', indicating an error when + // converting from one type to another + // With data NOT encrypted, converting string types to other + // types will not return '22018' + $success = ($errors[0]['SQLSTATE'] === '42000'); + if (!$success) { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + + // insert a row + // Take the second and third entres from the various $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'varchar(max)', use $varchar_params[1] and $varchar_params[2] + // to form an array + $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); + $r; + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_ constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt b/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt new file mode 100644 index 00000000..425d7f58 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt @@ -0,0 +1,278 @@ +--TEST-- +Prepare with cursor buffered and fetch a variety of types converted to different types +--DESCRIPTION-- +Test various conversion functionalites for buffered queries with SQLSRV. +--SKIPIF-- + +--FILE-- +SQLSRV_CURSOR_CLIENT_BUFFERED)); + if (!$stmt) { + fatalError("In fetchAsUTF8: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsUTF8: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as UTF-8 strings + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING('utf-8')); + if ($i == 0) { + if ($inputs[$i] !== hex2bin($f)) { + var_dump($f); + } + } else { + if ($f !== $inputs[$i]) { + var_dump($f); + } + } + } +} + +function fetchArray($conn, $tableName, $inputs) +{ + $query = "SELECT * FROM $tableName"; + + $stmt = sqlsrv_prepare($conn, $query, array(), array('Scrollable'=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchArray: failed to prepare query!"); + } + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("In fetchArray: failed to execute query!"); + } + + // Fetch fields as an array + $results = sqlsrv_fetch_array($stmt); + if ($results === false) { + fatalError("In fetchArray: failed to fetch the row from $tableName!"); + } + + for ($i = 0; $i < count($inputs); $i++) { + if ($i == 1) { + $expected = intval($inputs[$i]); + } elseif ($i == 2) { + $expected = floatval($inputs[$i]); + } else { + $expected = $inputs[$i]; + } + + if ($results[$i] !== $expected) { + echo "in fetchArray: for column $i expected $expected but got: "; + var_dump($results[$i]); + } + } +} + +function fetchAsFloats($conn, $tableName, $inputs) +{ + global $violation, $outOfRange, $epsilon; + + $query = "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $query, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchAsFloats: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsFloats: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as floats + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_FLOAT); + + if ($i == 0) { + // The varbinary field - expect the violation error + if (strpos(sqlsrv_errors()[0]['message'], $violation) === false) { + var_dump($f); + fatalError("in fetchAsFloats: expected $violation for column $i\n"); + } + } elseif ($i < 5) { + $expected = floatval($inputs[$i]); + $diff = abs(($f - $expected) / $expected); + + if ($diff > $epsilon) { + echo "in fetchAsFloats: for column $i expected $expected but got: "; + var_dump($f); + } + } else { + // The char fields will get errors too + // TODO 11297: fix this part outside Windows later + if (isWindows()) { + if (strpos(sqlsrv_errors()[0]['message'], $outOfRange) === false) { + var_dump($f); + fatalError("in fetchAsFloats: expected $outOfRange for column $i\n"); + } + } else { + if ($f != 0.0) { + var_dump($f); + } + } + } + } +} + +function fetchAsInts($conn, $tableName, $inputs) +{ + global $violation, $outOfRange, $truncation; + + $query = "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $query, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchAsInts: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsInts: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as integers + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_INT); + + if ($i == 0) { + // The varbinary field - expect the violation error + if (strpos(sqlsrv_errors()[0]['message'], $violation) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $violation for column $i\n"); + } + } elseif ($i == 2) { + // The float field - expect truncation + if (strpos(sqlsrv_errors()[0]['message'], $truncation) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $truncation for column $i\n"); + } + } elseif ($i >= 5) { + // The char fields will get errors too + // TODO 11297: fix this part outside Windows later + if (isWindows()) { + if (strpos(sqlsrv_errors()[0]['message'], $outOfRange) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $outOfRange for column $i\n"); + } + } else { + if ($f != 0) { + var_dump($f); + } + } + } else { + $expected = floor($inputs[$i]); + if ($f != $expected) { + echo "in fetchAsInts: for column $i expected $expected but got: "; + var_dump($f); + } + } + } +} + +function fetchAsBinary($conn, $tableName, $inputs) +{ + $query = "SELECT c_varbinary FROM $tableName"; + + $stmt = sqlsrv_prepare($conn, $query, array(), array('Scrollable'=>SQLSRV_CURSOR_CLIENT_BUFFERED)); + if (!$stmt) { + fatalError("In fetchAsBinary: failed to prepare query!"); + } + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("In fetchAsBinary: failed to execute query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsInts: failed to fetch the row from $tableName!"); + } + + // Fetch the varbinary field as is + $f = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM("binary")); + if (gettype($f) !== 'resource') { + var_dump($f); + } + // Do not expect errors + $errs = sqlsrv_errors(); + if (!empty($errs)) { + var_dump($errs); + } + + // Check its value + while (!feof($f)) { + $str = fread($f, 80); + } + if (trim($str) !== $inputs[0]) { + echo "Fetched binary value unexpected: $str\n"; + } +} + +require_once('MsCommon.inc'); + +$conn = AE\connect(array('CharacterSet' => 'UTF-8')); +$tableName = 'srvFetchingClientBuffer'; + +// Create table +$names = array('c_varbinary', 'c_int', 'c_float', 'c_decimal', 'c_datetime2', 'c_varchar', 'c_nvarchar'); + +$columns = array(new AE\ColumnMeta('varbinary(10)', $names[0]), + new AE\ColumnMeta('int', $names[1]), + new AE\ColumnMeta('float(53)', $names[2]), + new AE\ColumnMeta('decimal(16, 6)', $names[3]), + new AE\ColumnMeta('datetime2', $names[4]), + new AE\ColumnMeta('varchar(50)', $names[5]), + new AE\ColumnMeta('nvarchar(50)', $names[6])); +$stmt = AE\createTable($conn, $tableName, $columns); +if (!$stmt) { + fatalError("Failed to create $tableName!"); +} + +// Prepare the input values +$inputs = array('abcdefghij', '34567', '9876.5432', '123456789.012340', '2020-02-02 20:20:20.2220000', 'This is a test', 'Şơмė śäოрŀề'); + +$params = array(array(bin2hex($inputs[0]), SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_BINARY(10)), + $inputs[1], $inputs[2], $inputs[3], $inputs[4], $inputs[5], + array($inputs[6], SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'))); + +// Form the insert query +$colStr = '('; +foreach ($names as $name) { + $colStr .= $name . ", "; +} +$colStr = rtrim($colStr, ", ") . ") "; +$insertSql = "INSERT INTO [$tableName] " . $colStr . 'VALUES (?,?,?,?,?,?,?)'; + +// Insert one row only +$stmt = sqlsrv_prepare($conn, $insertSql, $params); +if ($stmt) { + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("Failed to execute insert statement to $tableName!"); + } +} else { + fatalError("Failed to prepare insert statement to $tableName!"); +} + +// Starting fetching using client buffers +fetchAsUTF8($conn, $tableName, $inputs); +fetchArray($conn, $tableName, $inputs); +fetchAsFloats($conn, $tableName, $inputs); +fetchAsInts($conn, $tableName, $inputs); +fetchAsBinary($conn, $tableName, $inputs); + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_commit_logs.phpt b/test/functional/sqlsrv/sqlsrv_commit_logs.phpt new file mode 100644 index 00000000..ba17483e --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_commit_logs.phpt @@ -0,0 +1,63 @@ +--TEST-- +Test sqlsrv_commit method with logging +--DESCRIPTION-- +Similar to sqlsrv_commit.phpt but also test some basic logging activities +By adding integer values together, we can specify more than one logging option at a time. +SQLSRV_LOG_SYSTEM_CONN (2) Turns on logging of connection activity. +SQLSRV_LOG_SYSTEM_STMT (4) Turns on logging of statement activity. + +For example, sqlsrv.LogSubsystems = 6 +turns on logging of connection and statement activities +--SKIPIF-- + +--FILE-- + +--EXPECT-- +sqlsrv_connect: entering +sqlsrv_query: entering +sqlsrv_query: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_query: entering +sqlsrv_query: entering +sqlsrv_commit: entering +sqlsrv_query: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_close: entering diff --git a/test/functional/sqlsrv/sqlsrv_connect.phpt b/test/functional/sqlsrv/sqlsrv_connect.phpt index 083e74fa..2dcacb7d 100644 --- a/test/functional/sqlsrv/sqlsrv_connect.phpt +++ b/test/functional/sqlsrv/sqlsrv_connect.phpt @@ -14,7 +14,7 @@ functions return FALSE for errors. fatalError("sqlsrv_connect should have returned false."); } - $conn = sqlsrv_connect("_!@#$", array( "Driver" => "Danica Patrick" )); + $conn = sqlsrv_connect("_!@#$", array( "Driver" => "Wrong Driver" )); if ($conn !== false) { fatalError("sqlsrv_connect should have returned false."); } diff --git a/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt b/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt index e6a5bbb6..f65b5810 100644 --- a/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt +++ b/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt @@ -1,98 +1,101 @@ --TEST-- -Test new connection keyword ColumnEncryption +Test new connection keyword ColumnEncryption with different input values +--DESCRIPTION-- +Some test cases return errors as expected. For testing purposes, an enclave enabled +SQL Server and the HGS server are the same instance. If the server is HGS enabled, +the error message of one test case is not the same. --SKIPIF-- --FILE-- $database,"UID"=>$userName, "PWD"=>$userPassword); -test_ColumnEncryption($server, $connectionOptions); +testColumnEncryption($server, $connectionOptions); echo "Done"; -function test_ColumnEncryption($server ,$connectionOptions){ +function testColumnEncryption($server, $connectionOptions) +{ $conn = sqlsrv_connect($server, $connectionOptions); - if ($conn === false) - { + if ($conn === false) { print_r(sqlsrv_errors()); } $msodbcsql_ver = sqlsrv_client_info($conn)['DriverVer']; - $msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; + $msodbcsqlMaj = explode(".", $msodbcsql_ver)[0]; + // Next, check if the server is HGS enabled + $hgsEnabled = true; + $serverInfo = sqlsrv_server_info($conn); + if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { + $hgsEnabled = false; + } + // Only works for ODBC 17 - $connectionOptions['ColumnEncryption']='Enabled'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - if($msodbcsql_maj < 17){ + $connectionOptions['ColumnEncryption'] = 'Enabled'; + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if ($msodbcsqlMaj < 17) { $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; - if( strcasecmp(sqlsrv_errors($conn)[0]['message'], $expected ) != 0 ) - { + if (strcasecmp(sqlsrv_errors($conn)[0]['message'], $expected) != 0) { print_r(sqlsrv_errors()); } - } - else - { + } else { + echo "Test case 1 failed:\n"; print_r(sqlsrv_errors()); } } - + // Works for ODBC 17, ODBC 13 $connectionOptions['ColumnEncryption']='Disabled'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - if($msodbcsql_maj < 13) - { - $expected_substr = "Invalid connection string attribute"; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if ($msodbcsqlMaj < 13) { + $expected = "Invalid connection string attribute"; + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { print_r(sqlsrv_errors()); } - } - else - { + } else { + echo "Test case 2 failed:\n"; print_r(sqlsrv_errors()); } - } - else - { + } else { sqlsrv_close($conn); } + + // Should fail for all ODBC drivers - but the error message returned depends on the server + $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; + if ($hgsEnabled) { + $expected = "Requested attestation protocol is invalid."; + } - // should fail for all ODBC drivers $connectionOptions['ColumnEncryption']='false'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 3 failed:\n"; print_r(sqlsrv_errors()); } } - // should fail for all ODBC drivers + $expected = "Invalid value type for option ColumnEncryption was specified. String type was expected."; + + // should fail for all ODBC drivers with the above error message $connectionOptions['ColumnEncryption']=true; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value type for option ColumnEncryption was specified. String type was expected."; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 4 failed:\n"; print_r(sqlsrv_errors()); } } - // should fail for all ODBC drivers + // should fail for all ODBC drivers with the above error message $connectionOptions['ColumnEncryption']=false; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value type for option ColumnEncryption was specified. String type was expected."; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 5 failed:\n"; print_r(sqlsrv_errors()); } } diff --git a/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt b/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt new file mode 100644 index 00000000..4422294f --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt @@ -0,0 +1,56 @@ +--TEST-- +Test functions return FALSE for errors with logging +--DESCRIPTION-- +Similar to sqlsrv_connect_logs.phpt but this time test logging to a log file +--SKIPIF-- + +--FILE-- + "Wrong Driver" )); + if ($conn !== false) { + fatalError("sqlsrv_connect should have returned false."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_NOTICE); + $conn = sqlsrv_connect($server, array( "uid" => $uid , "pwd" => $pwd )); + + if ($conn === false) { + fatalError("sqlsrv_connect should have connected."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); + $stmt = sqlsrv_query($conn, "SELECT * FROM some_bogus_table"); + if ($stmt !== false) { + fatalError("sqlsrv_query should have returned false."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_ALL); + if (file_exists($logFilepath)) { + echo file_get_contents($logFilepath); + unlink($logFilepath); + } + + sqlsrv_close($conn); +?> +--EXPECTF-- +[%s UTC] sqlsrv_connect: entering +[%s UTC] sqlsrv_connect: SQLSTATE = IMSSP +[%s UTC] sqlsrv_connect: error code = -106 +[%s UTC] sqlsrv_connect: message = Invalid value Wrong Driver was specified for Driver option. +[%s UTC] sqlsrv_connect: entering +[%s UTC] sqlsrv_query: SQLSTATE = 42S02 +[%s UTC] sqlsrv_query: error code = 208 +[%s UTC] sqlsrv_query: message = %s[SQL Server]Invalid object name 'some_bogus_table'. \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_connect_logs.phpt b/test/functional/sqlsrv/sqlsrv_connect_logs.phpt new file mode 100644 index 00000000..1ba654bd --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_connect_logs.phpt @@ -0,0 +1,49 @@ +--TEST-- +Test functions return FALSE for errors with logging +--DESCRIPTION-- +Similar to sqlsrv_connect.phpt but also test different settings of logging activities +--SKIPIF-- + +--FILE-- + "Wrong Driver" )); + if ($conn !== false) { + fatalError("sqlsrv_connect should have returned false."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_NOTICE); + $conn = sqlsrv_connect($server, array( "uid" => $uid , "pwd" => $pwd )); + + if ($conn === false) { + fatalError("sqlsrv_connect should have connected."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ERROR); + $stmt = sqlsrv_query($conn, "SELECT * FROM some_bogus_table"); + if ($stmt !== false) { + fatalError("sqlsrv_query should have returned false."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_WARNING); + + sqlsrv_close($conn); +?> +--EXPECTF-- +sqlsrv.LogSubsystems = -1 +sqlsrv_connect: entering +sqlsrv_connect: SQLSTATE = IMSSP +sqlsrv_connect: error code = -106 +sqlsrv_connect: message = Invalid value Wrong Driver was specified for Driver option. +sqlsrv_configure: entering +sqlsrv.LogSeverity = 4 +sqlsrv_connect: entering +sqlsrv_configure: entering +sqlsrv_query: SQLSTATE = 42S02 +sqlsrv_query: error code = 208 +sqlsrv_query: message = %s[SQL Server]Invalid object name 'some_bogus_table'. diff --git a/test/functional/sqlsrv/sqlsrv_get_field.phpt b/test/functional/sqlsrv/sqlsrv_get_field.phpt index 9247d0ba..9d63a1f5 100644 --- a/test/functional/sqlsrv/sqlsrv_get_field.phpt +++ b/test/functional/sqlsrv/sqlsrv_get_field.phpt @@ -268,7 +268,7 @@ NULL NULL 1 NULL -1\.0E\+37 +(1\.0E\+37|9.9999999999997E\+36) NULL 12\/12\/1968 04\:20\:00 NULL @@ -306,7 +306,7 @@ NULL NULL 0 NULL -\-1\.0E\+37 +(\-1\.0E\+37|-9.9999999999997E\+36) NULL 12\/12\/1968 04\:20\:00 NULL diff --git a/test/functional/sqlsrv/srv_007_login_timeout.phpt b/test/functional/sqlsrv/srv_007_login_timeout.phpt index eda8cf3a..8145c370 100644 --- a/test/functional/sqlsrv/srv_007_login_timeout.phpt +++ b/test/functional/sqlsrv/srv_007_login_timeout.phpt @@ -7,21 +7,24 @@ Intentionally provide an invalid server name and set LoginTimeout. Verify the ti --FILE-- $timeout)); $numAttempts++; @@ -38,7 +41,7 @@ do { echo "Connection failed at $elapsed secs. Leeway is $leeway sec but the difference is $diff\n"; } else { // The test will fail but this helps us decide if this test should be redesigned - echo "$numAttempts\t"; + echo "Attempts: $numAttempts, Time difference: $diff\n"; sleep(5); } } diff --git a/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt b/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt index 99a915da..1725d3b3 100644 --- a/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt +++ b/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt @@ -67,6 +67,10 @@ var_dump($array); $array = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); var_dump($array); +// The size of a float is platform dependent, with a precision of roughly 14 digits +// http://php.net/manual/en/language.types.float.php +$epsilon = 0.00001; + $numFields = sqlsrv_num_fields($stmt); $meta = sqlsrv_field_metadata($stmt); $rowcount = sqlsrv_num_rows($stmt); @@ -81,8 +85,21 @@ for ($i = 0; $i < $rowcount; $i++) { $field = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_INT); var_dump($field); } - $field = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_FLOAT); - var_dump($field); + $field1 = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_FLOAT); + if ($j > 5) { + // these are the zero fields + $expected = 0.0; + if ($field1 !== $expected) { + echo "Expected $expected but got $field1\n"; + } + } else { + $expected = floatval($field); + $diff = abs(($field1 - $expected) / $expected); + + if ($diff > $epsilon) { + echo "Expected $expected but got $field1 -- difference is $diff\n"; + } + } } } @@ -136,78 +153,60 @@ array(9) { column: a string(15) "1234567890.1234" -float(1234567890.1234) column: neg_a string(16) "-1234567890.1234" -float(-1234567890.1234) column: b string(1) "1" int(1) -float(1) column: neg_b string(2) "-1" int(-1) -float(-1) column: c string(7) ".500000" -float(0.5) column: neg_c string(8) "-.550000" -float(-0.55) column: zero string(1) "0" int(0) -float(0) column: zerof string(1) "0" -float(0) column: zerod string(7) ".000000" -float(0) column: a string(3) "0.5" -float(0.5) column: neg_a string(5) "-0.55" -float(-0.55) column: b string(6) "100000" int(100000) -float(100000) column: neg_b string(8) "-1234567" int(-1234567) -float(-1234567) column: c string(17) "1234567890.123400" -float(1234567890.1234) column: neg_c string(18) "-1234567890.123400" -float(-1234567890.1234) column: zero string(1) "0" int(0) -float(0) column: zerof string(1) "0" -float(0) column: zerod -string(7) ".000000" -float(0) +string(7) ".000000" \ No newline at end of file