diff --git a/CHANGELOG.md b/CHANGELOG.md index 0482dd3f..e04aab20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.10.0-beta1 - 2021-09-08 +Updated PECL release packages. Here is the list of updates: + +### Added +- Support for PHP 8.1 RC 1 +- Support for Ubuntu 21.04 and Alpine 3.13 +- Support for Apple M1 ARM64 hardware (requires [MS ODBC Driver 17.8+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-macos?view=sql-server-ver15) +- Feature Request [#795](https://github.com/microsoft/msphpsql/issues/795) - adding support for [Table-valued parameters](https://github.com/Microsoft/msphpsql/wiki/Features#tvp) + +### Removed +- Support for Ubuntu 16.04 + +### Fixed +- Issue [#1244](https://github.com/microsoft/msphpsql/issues/1244) - use lower case for object names for PDO::lastInsertId() - pull request [#1245](https://github.com/microsoft/msphpsql/pull/1245) by morozov +- Pull request [#1251](https://github.com/microsoft/msphpsql/pull/1251) - simplified implementations of last insert id and quote +- Issue [#1258](https://github.com/microsoft/msphpsql/issues/1258) - updated pdo_sqlsrv_get_driver_methods as per documentation - pull request [#1259](https://github.com/microsoft/msphpsql/pull/1259) +- Pull request [#1260](https://github.com/microsoft/msphpsql/pull/1260) - cleaned up redundant code +- Issue [#1261](https://github.com/microsoft/msphpsql/issues/1261) - simplified get_field_as_string and made it more robust - pull request [#1265](https://github.com/microsoft/msphpsql/pull/1265) +- Pull request [#1262](https://github.com/microsoft/msphpsql/pull/1262) - simplified parse_param_array in sqlsrv +- Pull request [#1267](https://github.com/microsoft/msphpsql/pull/1267) - replaced the obsolete MACRO ZVAL_NEW_ARR with array_init +- Pull request [#1275](https://github.com/microsoft/msphpsql/pull/1275) - fixed warning compiling core_stmt.cpp by mlocati +- Pull request [#1288](https://github.com/microsoft/msphpsql/pull/1288) - applied mask to pdo quote for binary inputs +- Pull request [#1290](https://github.com/microsoft/msphpsql/pull/1290) - updated list of supported processor architecture + +### 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 +- This release requires ODBC Driver 17.4.2 or above. Otherwise, a warning about failing to set an attribute may be suppressed when using an older ODBC driver. +- 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.9.0 - 2021-01-29 Updated PECL release packages. Here is the list of updates: diff --git a/Linux-mac-install.md b/Linux-mac-install.md index 7c2452e2..33034897 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,5 +1,5 @@ # 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 8.0, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 20.04, RedHat 7 and 8, Debian 9 and 10, Suse 12 and 15, Alpine 3.11 and 3.12, and macOS 10.14, 10.15, and 11.0. 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 8.0, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu, RedHat, Debian, Suse, Alpine, and macOS. 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 install PHP 8.0 by default using `pecl install`, if the PHP 8.0 packages are available. 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.4 or 7.3 instead. @@ -9,15 +9,15 @@ While these instructions contain commands to install both SQLSRV and PDO_SQLSRV ## Contents of this page -- [Installing the drivers on Ubuntu 16.04, 18.04, and 20.04](#installing-the-drivers-on-ubuntu-1604-1804-and-2004) +- [Installing the drivers on Ubuntu](#installing-the-drivers-on-ubuntu) - [Installing the drivers with PHP-FPM on Ubuntu](#installing-the-drivers-with-php-fpm-on-ubuntu) -- [Installing the drivers on Red Hat 7 and 8](#installing-the-drivers-on-red-hat-7-and-8) -- [Installing the drivers on Debian 9 and 10](#installing-the-drivers-on-debian-9-and-10) -- [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) -- [Installing the drivers on Alpine 3.11 and 3.12](#installing-the-drivers-on-alpine-311-and-312) -- [Installing the drivers on macOS Mojave, Catalina and Big Sur](#installing-the-drivers-on-macos-mojave-catalina-and-big-sur) +- [Installing the drivers on Red Hat](#installing-the-drivers-on-red-hat) +- [Installing the drivers on Debian](#installing-the-drivers-on-debian) +- [Installing the drivers on Suse](#installing-the-drivers-on-suse) +- [Installing the drivers on Alpine](#installing-the-drivers-on-alpine) +- [Installing the drivers on macOS](#installing-the-drivers-on-macos) -## Installing the drivers on Ubuntu 16.04, 18.04, and 20.04 +## Installing the drivers on Ubuntu > [!NOTE] > To install PHP 7.4 or 7.3, replace 8.0 with 7.4 or 7.3 in the following commands. @@ -127,7 +127,7 @@ sudo systemctl restart nginx.service ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Red Hat 7 and 8 +## Installing the drivers on Red Hat ### Step 1. Install PHP @@ -193,7 +193,7 @@ sudo apachectl restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Debian 9 and 10 +## Installing the drivers on Debian > [!NOTE] > To install PHP 7.4 or 7.3, replace 8.0 in the following commands with 7.4 or 7.3. @@ -245,7 +245,7 @@ sudo service apache2 restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Suse 12 and 15 +## Installing the drivers on Suse > [!NOTE] > In the following instructions, replace `` with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15_SP1 or SLE_15_SP2. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or check `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. @@ -292,7 +292,7 @@ sudo systemctl restart apache2 ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Alpine 3.11 and 3.12 +## Installing the drivers on Alpine > [!NOTE] > The default version of PHP is 7.3. PHP 7.4 or above may be available from testing or edge repositories for Alpine. You can instead compile PHP from source. @@ -332,13 +332,16 @@ sudo rc-service apache2 restart To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on macOS Mojave, Catalina and Big Sur +## Installing the drivers on macOS -If you do not already have it, install brew as follows: +If you do not already have it, install Homebrew as follows: ```bash /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ``` +> [!NOTE] +> If using Apple M1 ARM64 hardware, please install Homebrew and PHP directly without using the emulator Rosetta 2. + > [!NOTE] > To install PHP 7.4 or 7.3, replace php@8.0 with php@7.4 or php@7.3 respectively in the following commands. @@ -358,6 +361,9 @@ brew link --force --overwrite php@8.0 Install the ODBC driver for macOS by following the instructions on the [Install the Microsoft ODBC driver for SQL Server (macOS)]( https://docs.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-macos?view=sql-server-ver15). +> [!NOTE] +> If using Apple M1 ARM64 hardware, please install Microsoft ODBC driver 17.8+ directly without using the emulator Rosetta 2. + In addition, you may need to install the GNU make tools: ```bash brew install autoconf automake libtool @@ -368,7 +374,18 @@ brew install autoconf automake libtool sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv ``` + +If using Apple M1 ARM64, do the following instead: +```bash +sudo CXXFLAGS="-I/opt/homebrew/opt/unixodbc/include/" LDFLAGS="-L/opt/homebrew/lib/" pecl install sqlsrv +sudo CXXFLAGS="-I/opt/homebrew/opt/unixodbc/include/" LDFLAGS="-L/opt/homebrew/lib/" pecl install pdo_sqlsrv +``` + ### Step 4. Install Apache and configure driver loading + +> [!NOTE] +> The latest macOS 11.0 Big Sur comes with Apache 2.4 pre-installed, but Apple has also removed some required scripts. The solution is to install Apache 2.4 via Homebrew and then configure it, but this is out of scope for this installation guide, so please check Apache or Homebrew for detailed instructions. + ```bash brew install apache2 ``` diff --git a/README.md b/README.md index b1670468..6ba07a1f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Welcome to the Microsoft Drivers for PHP for Microsoft SQL Server** -The Microsoft Drivers for PHP for Microsoft SQL Server are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2012 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server][odbcdoc] to handle the low-level communication with SQL Server. +The [Microsoft Drivers for PHP for Microsoft SQL Server][phpdoc] are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2012 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server][odbcdoc] to handle the low-level communication with SQL Server. This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.3+ with improvements on both drivers and some limitations. Upcoming [releases][releases] will contain additional functionalities, bug fixes, and more. @@ -23,7 +23,7 @@ Azure Pipelines | AppVeyor (Windows) | Travis CI (Linux) | Co [tv-site]: https://travis-ci.org/microsoft/msphpsql/ [az-site]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_build/latest?definitionId=6&branchName=dev [az-image]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_apis/build/status/Microsoft.msphpsql?branchName=dev -[Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev +[Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev&&service=github [coveralls-site]: https://coveralls.io/github/microsoft/msphpsql?branch=dev [Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/dev/graph/badge.svg [codecov-site]: https://codecov.io/gh/microsoft/msphpsql @@ -66,7 +66,7 @@ Finally, if running PHP in a Web server, restart the Web server. For full instructions on installing the drivers on all supported Unix platforms, see [the installation instructions on Microsoft Docs][unixinstructions]. ## Sample Code -For PHP code samples, please see the [sample](https://github.com/Microsoft/msphpsql/tree/master/sample) folder or the [code samples on Microsoft Docs](https://docs.microsoft.com/sql/connect/php/code-samples-for-php-sql-driver). +For PHP code samples, please see the [sample](https://github.com/Microsoft/msphpsql/tree/master/sample) folder or the [code samples on Microsoft Docs](https://docs.microsoft.com/sql/connect/php/code-samples-for-php-sql-driver). For information on how to use the driver, see [Microsoft Drivers for PHP for Microsoft SQL Server][phpdoc]. ## Limitations and Known Issues Please refer to [Releases][releases] for the latest limitations and known issues. diff --git a/appveyor.yml b/appveyor.yml index f672aa84..9a8b75d9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,7 +30,7 @@ environment: SQL_INSTANCE: SQL2019 PHP_VC: vs16 PHP_MAJOR_VER: 8.0 - PHP_MINOR_VER: 0RC2 + PHP_MINOR_VER: latest PHP_EXE_PATH: Release THREAD: nts platform: x86 @@ -79,7 +79,7 @@ install: } - echo Downloading MSODBCSQL 17 # AppVeyor build works are x64 VMs and 32-bit ODBC driver cannot be installed on it - - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/6/b/3/6b3dd05c-678c-4e6b-b503-1d66e16ef23d/en-US/17.6.1.1/x64/msodbcsql.msi', 'c:\projects\msodbcsql.msi') + - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/a/e/b/aeb7d4ff-ca20-45db-86b8-8a8f774ce97b/en-US/17.8.1.1/x64/msodbcsql.msi', 'c:\projects\msodbcsql.msi') - cmd /c start /wait msiexec /i "c:\projects\msodbcsql.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL - echo Checking the version of MSODBCSQL - reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" @@ -116,10 +116,13 @@ test_script: - python %APPVEYOR_BUILD_FOLDER%\test\functional\setup\setup_dbs.py -dbname %SQLSRV_DBNAME% - Echo setup test database for PDO_SQLSRV tests - %PDOSQLSRV_DBNAME% - python %APPVEYOR_BUILD_FOLDER%\test\functional\setup\setup_dbs.py -dbname %PDOSQLSRV_DBNAME% + # for code coverage - exclude the following files: + # core_init.cpp, which primarily consists module initialization and shutdown + # core_stream.cpp for pdo_sqlsrv because it is only used by sqlsrv driver - ps: >- If ($env:BUILD_PLATFORM -Match "x86") { Write-Host "Running phpt tests via OpenCppCoverage..." - OpenCppCoverage.exe --sources ${env:PHP_SRC_DIR}\*sqlsrv --modules ${env:PHP_EXE_PATH}\php*sqlsrv.dll --excluded_sources ${env:PHP_SRC_DIR}\pdo_sqlsrv\shared\core_stream.cpp --export_type=cobertura:c:\projects\coverage.xml --quiet --cover_children --continue_after_cpp_exception --optimized_build -- .\php.exe run-tests.php -P --no-color ${env:APPVEYOR_BUILD_FOLDER}\test\functional\ | out-file -filePath ${env:APPVEYOR_BUILD_FOLDER}\test\functional\tests.log -encoding UTF8; + OpenCppCoverage.exe --sources ${env:PHP_SRC_DIR}\*sqlsrv --modules ${env:PHP_EXE_PATH}\php*sqlsrv.dll --excluded_sources core_init.cpp --excluded_sources ${env:PHP_SRC_DIR}\pdo_sqlsrv\shared\core_stream.cpp --export_type=cobertura:c:\projects\coverage.xml --quiet --cover_children --continue_after_cpp_exception --optimized_build -- .\php.exe run-tests.php -P --no-color ${env:APPVEYOR_BUILD_FOLDER}\test\functional\ | out-file -filePath ${env:APPVEYOR_BUILD_FOLDER}\test\functional\tests.log -encoding UTF8; Write-Host "Showing the last 25 lines of the log file..." Get-Content ${env:APPVEYOR_BUILD_FOLDER}\test\functional\tests.log -Tail 25; ls *.xml diff --git a/buildscripts/buildtools.py b/buildscripts/buildtools.py index ad555e2c..58177854 100644 --- a/buildscripts/buildtools.py +++ b/buildscripts/buildtools.py @@ -99,7 +99,7 @@ class BuildUtil(object): if self.vc == '': VC = 'vc15' version = self.version_label() - if version == '80': # Compiler version for PHP 8.0 or above + if version[0] == '8': # Compiler version for PHP 8.0 or above VC = 'vs16' self.vc = VC print('Compiler: ' + self.vc) diff --git a/source/pdo_sqlsrv/config.m4 b/source/pdo_sqlsrv/config.m4 index 7c488119..f91d84c0 100644 --- a/source/pdo_sqlsrv/config.m4 +++ b/source/pdo_sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.9 for PHP for SQL Server +dnl Microsoft Drivers 5.10 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index 1f3ed1a3..0ec718f5 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index e25168fd..a2e19e1b 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDO object for PDO_SQLSRV // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -34,7 +34,7 @@ namespace { const char LAST_INSERT_ID_QUERY[] = "SELECT @@IDENTITY;"; const size_t LAST_INSERT_ID_BUFF_LEN = 50; // size of the buffer to hold the string value of the last inserted id, which may be an int, bigint, decimal(p,0) or numeric(p,0) -const char SEQUENCE_CURRENT_VALUE_QUERY[] = "SELECT CURRENT_VALUE FROM SYS.SEQUENCES WHERE NAME=%s"; +const char SEQUENCE_CURRENT_VALUE_QUERY[] = "SELECT current_value FROM sys.sequences WHERE name=N'%s'"; const int LAST_INSERT_ID_QUERY_MAX_LEN = sizeof( SEQUENCE_CURRENT_VALUE_QUERY ) + SQL_MAX_SQLSERVERNAME + 2; // include the quotes // List of PDO supported connection options. @@ -436,36 +436,66 @@ const connection_option PDO_CONN_OPTS[] = { }; +#if PHP_VERSION_ID < 80100 // close the connection -int pdo_sqlsrv_dbh_close( _Inout_ pdo_dbh_t *dbh ); +int pdo_sqlsrv_dbh_close(_Inout_ pdo_dbh_t *dbh); // execute queries -int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const char *sql, - _Inout_ size_t sql_len, _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options ); -zend_long pdo_sqlsrv_dbh_do( _Inout_ pdo_dbh_t *dbh, _In_reads_bytes_(sql_len) const char *sql, _In_ size_t sql_len ); +int pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const char *sql, + _Inout_ size_t sql_len, _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options); +zend_long pdo_sqlsrv_dbh_do(_Inout_ pdo_dbh_t *dbh, _In_reads_bytes_(sql_len) const char *sql, _In_ size_t sql_len); + +// quote a string, meaning put quotes around it and escape any quotes within it +int pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_reads_(unquotedlen) const char* unquoted, _In_ size_t unquotedlen, _Outptr_result_buffer_(*quotedlen) char **quoted, _Out_ size_t* quotedlen, + enum pdo_param_type paramtype); // transaction support functions -int pdo_sqlsrv_dbh_commit( _Inout_ pdo_dbh_t *dbh ); -int pdo_sqlsrv_dbh_begin( _Inout_ pdo_dbh_t *dbh ); -int pdo_sqlsrv_dbh_rollback( _Inout_ pdo_dbh_t *dbh ); +int pdo_sqlsrv_dbh_commit(_Inout_ pdo_dbh_t *dbh); +int pdo_sqlsrv_dbh_begin(_Inout_ pdo_dbh_t *dbh); +int pdo_sqlsrv_dbh_rollback(_Inout_ pdo_dbh_t *dbh); // attribute functions -int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val ); -int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *return_value ); - -// return more information -int pdo_sqlsrv_dbh_return_error( _In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, - _Out_ zval *info); +int pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val); +int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *return_value); // return the last id generated by an executed SQL statement -char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, _Out_ size_t* len ); +char * pdo_sqlsrv_dbh_last_id(_Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, _Out_ size_t* len); + +// return more information +int pdo_sqlsrv_dbh_return_error(_In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, _Out_ zval *info); + +#else +// close the connection +void pdo_sqlsrv_dbh_close(_Inout_ pdo_dbh_t *dbh); + +// execute queries +bool pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_ zend_string *sql, + _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options); +zend_long pdo_sqlsrv_dbh_do(_Inout_ pdo_dbh_t *dbh, _In_ const zend_string *sql); + +// quote a string, meaning put quotes around it and escape any quotes within it +zend_string* pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_ const zend_string *unquoted, _In_ enum pdo_param_type paramtype); + +// transaction support functions +bool pdo_sqlsrv_dbh_commit(_Inout_ pdo_dbh_t *dbh); +bool pdo_sqlsrv_dbh_begin(_Inout_ pdo_dbh_t *dbh); +bool pdo_sqlsrv_dbh_rollback(_Inout_ pdo_dbh_t *dbh); + +// attribute functions +bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val); +int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *return_value); + +// return the last id generated by an executed SQL statement +zend_string * pdo_sqlsrv_dbh_last_id(_Inout_ pdo_dbh_t *dbh, _In_ const zend_string *name); + +// return more information +void pdo_sqlsrv_dbh_return_error(_In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, _Out_ zval *info); + +#endif // additional methods are supported in this function pdo_sqlsrv_function_entry *pdo_sqlsrv_get_driver_methods( _Inout_ pdo_dbh_t *dbh, int kind ); -// quote a string, meaning put quotes around it and escape any quotes within it -int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquotedlen) const char* unquoted, _In_ size_t unquotedlen, _Outptr_result_buffer_(*quotedlen) char **quoted, _Out_ size_t* quotedlen, - enum pdo_param_type paramtype ); struct pdo_dbh_methods pdo_sqlsrv_dbh_methods = { @@ -480,12 +510,17 @@ struct pdo_dbh_methods pdo_sqlsrv_dbh_methods = { pdo_sqlsrv_dbh_last_id, pdo_sqlsrv_dbh_return_error, pdo_sqlsrv_dbh_get_attr, - NULL, // check liveness not implemented + NULL, // check liveness not implemented pdo_sqlsrv_get_driver_methods, - NULL, // request shutdown not implemented - NULL // in transaction not implemented + NULL, // request shutdown not implemented +#if PHP_VERSION_ID < 80100 + NULL // in transaction not implemented }; - +#else + NULL, // in transaction not implemented + NULL // get_gc not implemented +}; +#endif // log a function entry point #define PDO_LOG_DBH_ENTRY \ @@ -632,15 +667,22 @@ int pdo_sqlsrv_db_handle_factory( _Inout_ pdo_dbh_t *dbh, _In_opt_ zval *driver_ // Parameters: // dbh - The PDO managed connection object. // Return: -// Always returns 1 for success. +// Always returns 1 for success. (for PHP_VERSION_ID < 80100) +#if PHP_VERSION_ID < 80100 int pdo_sqlsrv_dbh_close( _Inout_ pdo_dbh_t *dbh ) +#else +void pdo_sqlsrv_dbh_close(_Inout_ pdo_dbh_t *dbh) +#endif { LOG( SEV_NOTICE, "pdo_sqlsrv_dbh_close: entering" ); // if the connection didn't complete properly, driver_data isn't initialized. if( dbh->driver_data == NULL ) { - +#if PHP_VERSION_ID < 80100 return 1; +#else + return; +#endif } PDO_RESET_DBH_ERROR; @@ -649,8 +691,10 @@ int pdo_sqlsrv_dbh_close( _Inout_ pdo_dbh_t *dbh ) core_sqlsrv_close( reinterpret_cast( dbh->driver_data ) ); dbh->driver_data = NULL; +#if PHP_VERSION_ID < 80100 // always return success that the connection is closed return 1; +#endif } // pdo_sqlsrv_dbh_prepare @@ -664,8 +708,12 @@ int pdo_sqlsrv_dbh_close( _Inout_ pdo_dbh_t *dbh ) // driver_options - User provided list of statement options. // Return: // 0 for failure, 1 for success. -int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const char *sql, - _Inout_ size_t sql_len, _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options ) +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const char *sql, + _Inout_ size_t sql_len, _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options) +#else +bool pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_ zend_string *sql_zstr, _Inout_ pdo_stmt_t *stmt, _In_ zval *driver_options) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -707,27 +755,44 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch driver_stmt->buffered_query_limit = driver_dbh->client_buffer_max_size; } +#if PHP_VERSION_ID >= 80100 + zend_string* sql_rewrite_zstr = NULL; + const char* sql = ZSTR_VAL(sql_zstr); + size_t sql_len = ZSTR_LEN(sql_zstr); +#endif + // rewrite named parameters in the query to positional parameters if we aren't letting PDO do the // parameter substitution for us if( stmt->supports_placeholders != PDO_PLACEHOLDER_NONE ) { // rewrite the query to map named parameters to positional parameters. We do this rather than use the ODBC named // parameters for consistency with the PDO MySQL and PDO ODBC drivers. +#if PHP_VERSION_ID < 80100 int zr = pdo_parse_params( stmt, const_cast( sql ), sql_len, &sql_rewrite, &sql_rewrite_len ); - - CHECK_ZEND_ERROR( zr, driver_dbh, PDO_SQLSRV_ERROR_PARAM_PARSE) { + CHECK_ZEND_ERROR(zr, driver_dbh, PDO_SQLSRV_ERROR_PARAM_PARSE) { throw core::CoreException(); } // if parameter substitution happened, use that query instead of the original - if( sql_rewrite != 0) { + if (sql_rewrite != 0) { sql = sql_rewrite; sql_len = sql_rewrite_len; } +#else + int zr = pdo_parse_params(stmt, sql_zstr, &sql_rewrite_zstr); + CHECK_ZEND_ERROR(zr, driver_dbh, PDO_SQLSRV_ERROR_PARAM_PARSE) { + throw core::CoreException(); + } + + // if parameter substitution happened, use that query instead of the original + if (sql_rewrite_zstr != NULL) { + sql = ZSTR_VAL(sql_rewrite_zstr); + sql_len = ZSTR_LEN(sql_rewrite_zstr); + } +#endif } if( !driver_stmt->direct_query && stmt->supports_placeholders != PDO_PLACEHOLDER_NONE ) { - - core_sqlsrv_prepare( driver_stmt, sql, sql_len ); + core_sqlsrv_prepare(driver_stmt, sql, sql_len); } else if( driver_stmt->direct_query ) { @@ -739,14 +804,26 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch driver_stmt->direct_query_subst_string = estrdup( sql ); driver_stmt->direct_query_subst_string_len = sql_len; } + +#if PHP_VERSION_ID >= 80100 + if (sql_rewrite_zstr != NULL) { + zend_string_release(sql_rewrite_zstr); + } +#endif + // else if stmt->support_placeholders == PDO_PLACEHOLDER_NONE means that stmt->active_query_string will be // set to the substituted query if ( stmt->supports_placeholders == PDO_PLACEHOLDER_NONE ) { - // parse placeholders in the sql query into the placeholders ht + // parse placeholders in the sql query into the placeholders ht ALLOC_HASHTABLE( placeholders ); - core::sqlsrv_zend_hash_init( *driver_dbh, placeholders, 5, ZVAL_PTR_DTOR /* dtor */, 0 /* persistent */ ); - sql_parser = new ( sqlsrv_malloc( sizeof( sql_string_parser ))) sql_string_parser( *driver_dbh, stmt->query_string, - static_cast(stmt->query_stringlen), placeholders ); + core::sqlsrv_zend_hash_init(*driver_dbh, placeholders, 5, ZVAL_PTR_DTOR /* dtor */, 0 /* persistent */); +#if PHP_VERSION_ID < 80100 + sql_parser = new (sqlsrv_malloc(sizeof(sql_string_parser))) sql_string_parser(*driver_dbh, stmt->query_string, + static_cast(stmt->query_stringlen), placeholders); +#else + sql_parser = new (sqlsrv_malloc(sizeof(sql_string_parser))) sql_string_parser(*driver_dbh, ZSTR_VAL(stmt->query_string), + ZSTR_LEN(stmt->query_string), placeholders); +#endif sql_parser->parse_sql_string(); driver_stmt->placeholders = placeholders; placeholders.transferred(); @@ -772,7 +849,11 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch reinterpret_cast( driver_dbh->last_error()->sqlstate )); } +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } // catch any errant exception and die @@ -781,7 +862,11 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch DIE( "pdo_sqlsrv_dbh_prepare: Unknown exception caught." ); } +#if PHP_VERSION_ID < 80100 return 1; +#else + return true; +#endif } @@ -795,7 +880,11 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch // sql_len - length of sql query // Return // # of rows affected, -1 for an error. +#if PHP_VERSION_ID < 80100 zend_long pdo_sqlsrv_dbh_do( _Inout_ pdo_dbh_t *dbh, _In_reads_bytes_(sql_len) const char *sql, _In_ size_t sql_len ) +#else +zend_long pdo_sqlsrv_dbh_do(_Inout_ pdo_dbh_t *dbh, _In_ const zend_string *sql) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -823,8 +912,11 @@ zend_long pdo_sqlsrv_dbh_do( _Inout_ pdo_dbh_t *dbh, _In_reads_bytes_(sql_len) c NULL /*valid_stmt_opts*/, pdo_sqlsrv_handle_stmt_error, &temp_stmt ); driver_stmt->set_func( __FUNCTION__ ); - SQLRETURN execReturn = core_sqlsrv_execute( driver_stmt, sql, static_cast( sql_len ) ); - +#if PHP_VERSION_ID < 80100 + SQLRETURN execReturn = core_sqlsrv_execute(driver_stmt, sql, static_cast(sql_len)); +#else + SQLRETURN execReturn = core_sqlsrv_execute(driver_stmt, ZSTR_VAL(sql), ZSTR_LEN(sql)); +#endif // since the user can give us a compound statement, we return the row count for the last set, and since the row count // isn't guaranteed to be valid until all the results have been fetched, we fetch them all first. @@ -883,8 +975,13 @@ zend_long pdo_sqlsrv_dbh_do( _Inout_ pdo_dbh_t *dbh, _In_reads_bytes_(sql_len) c // Parameters: // dbh - The PDO managed connection object. // Return: -// 0 for failure and 1 for success. -int pdo_sqlsrv_dbh_begin( _Inout_ pdo_dbh_t *dbh ) +// 0 for failure and 1 for success. (if PHP_VERSION_ID < 80100) +// Return true if currently inside a transaction, false otherwise +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_begin(_Inout_ pdo_dbh_t *dbh) +#else +bool pdo_sqlsrv_dbh_begin(_Inout_ pdo_dbh_t *dbh) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -902,18 +999,29 @@ int pdo_sqlsrv_dbh_begin( _Inout_ pdo_dbh_t *dbh ) core_sqlsrv_begin_transaction( driver_conn ); +#if PHP_VERSION_ID < 80100 return 1; +#else + return true; +#endif } catch( core::CoreException& ) { - +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } catch( ... ) { DIE ("pdo_sqlsrv_dbh_begin: Uncaught exception occurred."); } // Should not have reached here but adding this due to compilation warnings +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } @@ -927,8 +1035,13 @@ int pdo_sqlsrv_dbh_begin( _Inout_ pdo_dbh_t *dbh ) // Parameters: // dbh - The PDO managed connection object. // Return: -// 0 for failure and 1 for success. -int pdo_sqlsrv_dbh_commit( _Inout_ pdo_dbh_t *dbh ) +// 0 for failure and 1 for success. (if PHP_VERSION_ID < 80100) +// Return true for success and false otherwise +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_commit(_Inout_ pdo_dbh_t *dbh) +#else +bool pdo_sqlsrv_dbh_commit(_Inout_ pdo_dbh_t *dbh) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -946,18 +1059,30 @@ int pdo_sqlsrv_dbh_commit( _Inout_ pdo_dbh_t *dbh ) core_sqlsrv_commit( driver_conn ); +#if PHP_VERSION_ID < 80100 return 1; +#else + return true; +#endif } catch( core::CoreException& ) { - +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } catch( ... ) { DIE ("pdo_sqlsrv_dbh_commit: Uncaught exception occurred."); } + // Should not have reached here but adding this due to compilation warnings +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } // pdo_sqlsrv_dbh_rollback @@ -969,8 +1094,13 @@ int pdo_sqlsrv_dbh_commit( _Inout_ pdo_dbh_t *dbh ) // Parameters: // dbh - The PDO managed connection object. // Return: -// 0 for failure and 1 for success. -int pdo_sqlsrv_dbh_rollback( _Inout_ pdo_dbh_t *dbh ) +// 0 for failure and 1 for success. (if PHP_VERSION_ID < 80100) +// Return true for success and false otherwise +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_rollback(_Inout_ pdo_dbh_t *dbh) +#else +bool pdo_sqlsrv_dbh_rollback(_Inout_ pdo_dbh_t *dbh) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -987,18 +1117,30 @@ int pdo_sqlsrv_dbh_rollback( _Inout_ pdo_dbh_t *dbh ) core_sqlsrv_rollback( driver_conn ); +#if PHP_VERSION_ID < 80100 return 1; +#else + return true; +#endif } catch( core::CoreException& ) { +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } catch( ... ) { DIE ("pdo_sqlsrv_dbh_rollback: Uncaught exception occurred."); } // Should not have reached here but adding this due to compilation warnings +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } // pdo_sqlsrv_dbh_set_attr @@ -1010,8 +1152,13 @@ int pdo_sqlsrv_dbh_rollback( _Inout_ pdo_dbh_t *dbh ) // attr - The attribute to be set. // val - The value of the attribute to be set. // Return: -// 0 for failure, 1 for success. -int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val ) +// 0 for failure, 1 for success. (if PHP_VERSION_ID < 80100) +// Return true on success and false in case of failure +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val) +#else +bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *val) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -1049,7 +1196,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout break; case SQLSRV_ATTR_DIRECT_QUERY: - driver_dbh->direct_query = ( zend_is_true( val ) ) ? true : false; + driver_dbh->direct_query = zend_is_true(val); break; case SQLSRV_ATTR_QUERY_TIMEOUT: @@ -1069,15 +1216,15 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout break; case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: - driver_dbh->fetch_numeric = (zend_is_true(val)) ? true : false; + driver_dbh->fetch_numeric = zend_is_true(val); break; case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: - driver_dbh->fetch_datetime = (zend_is_true(val)) ? true : false; + driver_dbh->fetch_datetime = zend_is_true(val); break; case SQLSRV_ATTR_FORMAT_DECIMALS: - driver_dbh->format_decimals = (zend_is_true(val)) ? true : false; + driver_dbh->format_decimals = zend_is_true(val); break; case SQLSRV_ATTR_DECIMAL_PLACES: @@ -1158,11 +1305,18 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout } } catch( pdo::PDOException& ) { - +#if PHP_VERSION_ID < 80100 return 0; +#else + return false; +#endif } +#if PHP_VERSION_ID < 80100 return 1; +#else + return true; +#endif } @@ -1173,8 +1327,12 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout // attr - The attribute to get. // return_value - zval in which to return the attribute value. // Return: -// 0 for failure, 1 for success. -int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *return_value ) +// 0 for failure, 1 for success. (if PHP_VERSION_ID < 80100) +// There are 3 return states: +// -1 for errors while retrieving a valid attribute +// 0 for attempting to retrieve an attribute which is not supported by the driver +// any other value for success, *return_value must be set to the attribute value +int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ zval *return_value) { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -1196,8 +1354,12 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_AUTOCOMMIT: case PDO_ATTR_TIMEOUT: { +#if PHP_VERSION_ID < 80100 // PDO does not throw "not supported" error message for these attributes. - THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_UNSUPPORTED_DBH_ATTR ); + THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_UNSUPPORTED_DBH_ATTR); +#else + return 0; +#endif } // Statement level only @@ -1303,10 +1465,18 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout } } +#if PHP_VERSION_ID < 80100 return 1; +#else + return 1; +#endif } catch( core::CoreException& ) { +#if PHP_VERSION_ID < 80100 return 0; +#else + return -1; +#endif } } @@ -1318,8 +1488,11 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout // info - zval in which to return the error info. // Return: // 0 for failure, 1 for success. -int pdo_sqlsrv_dbh_return_error( _In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, - _Out_ zval *info) +#if PHP_VERSION_ID < 80100 +int pdo_sqlsrv_dbh_return_error(_In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, _Out_ zval *info) +#else +void pdo_sqlsrv_dbh_return_error(_In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, _Out_ zval *info) +#endif { SQLSRV_ASSERT( dbh != NULL || stmt != NULL, "Either dbh or stmt must not be NULL to dereference the error." ); @@ -1333,7 +1506,9 @@ int pdo_sqlsrv_dbh_return_error( _In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, pdo_sqlsrv_retrieve_context_error( ctx_error, info ); +#if PHP_VERSION_ID < 80100 return 1; +#endif } // pdo_sqlsrv_dbh_last_id @@ -1344,8 +1519,13 @@ int pdo_sqlsrv_dbh_return_error( _In_ pdo_dbh_t *dbh, _In_opt_ pdo_stmt_t *stmt, // name - Table name. // len - Length of the name. // Return: -// Returns the last insert id as a string. -char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, _Out_ size_t* len ) +// Returns the last insert id as a string. (if PHP_VERSION_ID < 80100) +// Returning NULL indicates an error condition. The input "name" MIGHT be NULL +#if PHP_VERSION_ID < 80100 +char * pdo_sqlsrv_dbh_last_id(_Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, _Out_ size_t* len) +#else +zend_string * pdo_sqlsrv_dbh_last_id(_Inout_ pdo_dbh_t *dbh, _In_ const zend_string *name) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -1360,22 +1540,28 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, pdo_sqlsrv_dbh* driver_dbh = static_cast( dbh->driver_data ); SQLSRV_ASSERT( driver_dbh != NULL, "pdo_sqlsrv_dbh_last_id: driver_data object was NULL." ); - sqlsrv_malloc_auto_ptr id_str; - id_str = reinterpret_cast( sqlsrv_malloc( LAST_INSERT_ID_BUFF_LEN )); + char idSTR[LAST_INSERT_ID_BUFF_LEN] = { '\0' }; + char* str = NULL; + SQLLEN cbID = 0; try { + sqlsrv_malloc_auto_ptr wsql_string; + unsigned int wsql_len; - char last_insert_id_query[LAST_INSERT_ID_QUERY_MAX_LEN] = {'\0'}; - if( name == NULL ) { - strcpy_s( last_insert_id_query, sizeof( last_insert_id_query ), LAST_INSERT_ID_QUERY ); + if (name == NULL) { + wsql_string = utf16_string_from_mbcs_string(SQLSRV_ENCODING_CHAR, LAST_INSERT_ID_QUERY, sizeof(LAST_INSERT_ID_QUERY), &wsql_len); + } else { + char buffer[LAST_INSERT_ID_QUERY_MAX_LEN] = { '\0' }; +#if PHP_VERSION_ID < 80100 + snprintf(buffer, LAST_INSERT_ID_QUERY_MAX_LEN, SEQUENCE_CURRENT_VALUE_QUERY, name); +#else + const char *name_str = ZSTR_VAL(name); + snprintf(buffer, LAST_INSERT_ID_QUERY_MAX_LEN, SEQUENCE_CURRENT_VALUE_QUERY, ZSTR_VAL(name)); +#endif + wsql_string = utf16_string_from_mbcs_string(SQLSRV_ENCODING_CHAR, buffer, sizeof(buffer), &wsql_len); } - else { - char* quoted_table = NULL; - size_t quoted_len = 0; - int quoted = pdo_sqlsrv_dbh_quote( dbh, name, strnlen_s( name ), "ed_table, "ed_len, PDO_PARAM_NULL ); - SQLSRV_ASSERT( quoted, "PDO::lastInsertId failed to quote the table name."); - snprintf( last_insert_id_query, LAST_INSERT_ID_QUERY_MAX_LEN, SEQUENCE_CURRENT_VALUE_QUERY, quoted_table ); - sqlsrv_free( quoted_table ); + CHECK_CUSTOM_ERROR(wsql_string == 0, driver_stmt, SQLSRV_ERROR_QUERY_STRING_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); } // temp PDO statement used for error handling if something happens @@ -1386,31 +1572,18 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, driver_stmt = core_sqlsrv_create_stmt( driver_dbh, core::allocate_stmt, NULL /*options_ht*/, NULL /*valid_stmt_opts*/, pdo_sqlsrv_handle_stmt_error, &temp_stmt ); driver_stmt->set_func( __FUNCTION__ ); - - sqlsrv_malloc_auto_ptr wsql_string; - unsigned int wsql_len; - wsql_string = utf16_string_from_mbcs_string( SQLSRV_ENCODING_CHAR, reinterpret_cast( last_insert_id_query ), sizeof(last_insert_id_query), &wsql_len ); - - CHECK_CUSTOM_ERROR( wsql_string == 0, driver_stmt, SQLSRV_ERROR_QUERY_STRING_ENCODING_TRANSLATE, get_last_error_message() ) { - throw core::CoreException(); - } - - // execute the last insert id query + // execute the last insert id query core::SQLExecDirectW( driver_stmt, wsql_string ); - core::SQLFetchScroll( driver_stmt, SQL_FETCH_NEXT, 0 ); - SQLRETURN r = core::SQLGetData( driver_stmt, 1, SQL_C_CHAR, id_str, LAST_INSERT_ID_BUFF_LEN, - reinterpret_cast( len ), false ); - CHECK_CUSTOM_ERROR( (!SQL_SUCCEEDED( r ) || *len == SQL_NULL_DATA || *len == SQL_NO_TOTAL), driver_stmt, - PDO_SQLSRV_ERROR_LAST_INSERT_ID ) { + SQLRETURN r = core::SQLGetData(driver_stmt, 1, SQL_C_CHAR, idSTR, LAST_INSERT_ID_BUFF_LEN, &cbID, false); + CHECK_CUSTOM_ERROR((!SQL_SUCCEEDED(r) || cbID == SQL_NULL_DATA || cbID == SQL_NO_TOTAL), driver_stmt, + PDO_SQLSRV_ERROR_LAST_INSERT_ID) { throw core::CoreException(); } driver_stmt->~sqlsrv_stmt(); - } - catch( core::CoreException& ) { - + } catch( core::CoreException& ) { // copy any errors on the statement to the connection so that the user sees them, since the statement is released // before this method returns strcpy_s( dbh->error_code, sizeof( dbh->error_code ), @@ -1420,18 +1593,31 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, if( driver_stmt ) { driver_stmt->~sqlsrv_stmt(); } - - strcpy_s( id_str.get(), 1, "" ); +#if PHP_VERSION_ID < 80100 *len = 0; + str = reinterpret_cast(sqlsrv_malloc(0, sizeof(char), 1)); // return an empty string with a null terminator + str[0] = '\0'; + return str; +#else + return NULL; +#endif } - char* ret_id_str = id_str.get(); - id_str.transferred(); - // restore error handling to its previous mode dbh->error_mode = prev_err_mode; - return ret_id_str; + // copy the last ID string and return it + str = reinterpret_cast(sqlsrv_malloc(cbID, sizeof(char), 1)); // include space for null terminator + strcpy_s(str, cbID + 1, idSTR); + +#if PHP_VERSION_ID < 80100 + *len = static_cast(cbID); + return str; +#else + zend_string *zstr = zend_string_init(str, cbID, 0); + sqlsrv_free(str); + return zstr; +#endif } // pdo_sqlsrv_dbh_quote @@ -1445,8 +1631,12 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, // quoted_len - Length of the output string. // Return: // 0 for failure, 1 for success. +#if PHP_VERSION_ID < 80100 int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const char* unquoted, _In_ size_t unquoted_len, _Outptr_result_buffer_(*quoted_len) char **quoted, _Out_ size_t* quoted_len, enum pdo_param_type paramtype ) +#else +zend_string* pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_ const zend_string *unquoted, _In_ enum pdo_param_type paramtype) +#endif { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; @@ -1523,85 +1713,113 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const } #endif - if ( encoding == SQLSRV_ENCODING_BINARY ) { - // convert from char* to hex digits using os - std::basic_ostringstream os; - for ( size_t index = 0; index < unquoted_len && unquoted[index] != '\0'; ++index ) { - // if unquoted is < 0 or > 255, that means this is a non-ascii character. Translation from non-ascii to binary is not supported. - // return an empty terminated string for now - if (( int )unquoted[index] < 0 || ( int )unquoted[index] > 255) { - *quoted_len = 0; - *quoted = reinterpret_cast( sqlsrv_malloc( *quoted_len, sizeof( char ), 1 )); - ( *quoted )[0] = '\0'; - return 1; + if (encoding == SQLSRV_ENCODING_BINARY) { +#if PHP_VERSION_ID < 80100 + *quoted_len = (unquoted_len * 2) + 2; // each character will be converted to 2 hex digits and prepend '0x' to the result + *quoted = reinterpret_cast(sqlsrv_malloc(*quoted_len, sizeof(char), 1)); // include space for null terminator + memset(*quoted, '\0', *quoted_len + 1); + + unsigned int pos = 0; + (*quoted)[pos++] = '0'; + (*quoted)[pos++] = 'x'; + + for (size_t index = 0; index < unquoted_len && unquoted[index] != '\0'; ++index) { + // On success, snprintf returns the total number of characters written + // On failure, a negative number is returned + // The generated string has a length of at most len - 1, so + // len is 3 (2 hex digits + 1) + // Requires "& 0x000000FF", or snprintf will translate "0x90" to "0xFFFFFF90" + int n = snprintf((char*)(*quoted + pos), 3, "%02X", unquoted[index] & 0x000000FF); + if (n < 0) { + // Something went wrong, simply return 0 (failure) + return 0; } - // when an int is < 16 and is appended to os, its hex representation which starts - // with '0' does not get appended properly (the starting '0' does not get appended) - // thus append '0' first - if (( int )unquoted[index] < 16 ) { - os << '0'; - } - os << std::hex << ( int )unquoted[index]; + pos += 2; } - std::basic_string str_hex = os.str(); - // each character is represented by 2 digits of hex - size_t unquoted_str_len = unquoted_len * 2; // length returned should not account for null terminator - char* unquoted_str = reinterpret_cast( sqlsrv_malloc( unquoted_str_len, sizeof( char ), 1 )); // include space for null terminator - strcpy_s( unquoted_str, unquoted_str_len + 1 /* include null terminator*/, str_hex.c_str() ); - // include length of '0x' in the binary string - *quoted_len = unquoted_str_len + 2; - *quoted = reinterpret_cast( sqlsrv_malloc( *quoted_len, sizeof( char ), 1 )); - unsigned int out_current = 0; - // insert '0x' - ( *quoted )[out_current++] = '0'; - ( *quoted )[out_current++] = 'x'; - for ( size_t index = 0; index < unquoted_str_len && unquoted_str[index] != '\0'; ++index ) { - ( *quoted )[out_current++] = unquoted_str[index]; - } - // null terminator - ( *quoted )[out_current] = '\0'; - sqlsrv_free( unquoted_str ); + return 1; +#else + size_t unquoted_len = ZSTR_LEN(unquoted); + const char *unquoted_str = ZSTR_VAL(unquoted); + + sqlsrv_malloc_auto_ptr quoted; + size_t quoted_len = (unquoted_len * 2) + 2; // each character will be converted to 2 hex digits and prepend '0x' to the result + quoted = reinterpret_cast(sqlsrv_malloc(quoted_len, sizeof(char), 1)); // include space for null terminator + memset(quoted, '\0', quoted_len + 1); + + unsigned int pos = 0; + quoted[pos++] = '0'; + quoted[pos++] = 'x'; + + char *p = quoted; + for (size_t index = 0; index < unquoted_len && unquoted_str[index] != '\0'; ++index) { + // On success, snprintf returns the total number of characters written + // On failure, a negative number is returned + // The generated string has a length of at most len - 1, so + // len is 3 (2 hex digits + 1) + // Requires "& 0x000000FF", or snprintf will translate "0x90" to "0xFFFFFF90" + int n = snprintf((char*)(p + pos), 3, "%02X", unquoted_str[index] & 0x000000FF); + if (n < 0) { + // Something went wrong, simply return NULL (failure) + return NULL; + } + pos += 2; + } + + zend_string* zstr = zend_string_init(quoted, quoted_len, 0); + return zstr; +#endif } else { - // count the number of quotes needed - unsigned int quotes_needed = 2; // the initial start and end quotes of course - // include the N proceeding the initial quote if encoding is UTF8 - if (use_national_char_set) { - quotes_needed = 3; + // The minimum number of single quotes needed is 2 -- the initial start and end quotes + // Add the letter N before the initial quote if the encoding is UTF8 + int quotes_needed = (use_national_char_set) ? 3 : 2; + char c = '\''; + +#if PHP_VERSION_ID < 80100 + std::string tmp_str(unquoted, unquoted_len); // Copy all unquoted_len characters from unquoted +#else + size_t unquoted_len = ZSTR_LEN(unquoted); + const char *unquoted_str = ZSTR_VAL(unquoted); + std::string tmp_str(unquoted_str, unquoted_len); // Copy all unquoted_len characters from unquoted +#endif + + std::size_t found = tmp_str.find(c); // Find the first single quote + while (found != std::string::npos) { + tmp_str.insert(found + 1, 1, c); // Insert an additional single quote + found = tmp_str.find(c, found + 2); // Find the next single quote } - for ( size_t index = 0; index < unquoted_len; ++index ) { - if ( unquoted[index] == '\'' ) { - ++quotes_needed; - } + size_t len = tmp_str.length(); + +#if PHP_VERSION_ID < 80100 + *quoted_len = quotes_needed + len; // The new length should be number of quotes plus the length of tmp_str + *quoted = reinterpret_cast(sqlsrv_malloc(*quoted_len, sizeof(char), 1)); // include space for null terminator + memset(*quoted, '\0', *quoted_len + 1); + + char *p = *quoted; +#else + sqlsrv_malloc_auto_ptr quoted; + size_t quoted_len = quotes_needed + len; // length returned to the caller should not account for null terminator + quoted = reinterpret_cast(sqlsrv_malloc(quoted_len, sizeof(char), 1)); // include space for null terminator + memset(quoted, '\0', quoted_len + 1); + + char *p = quoted; +#endif + size_t pos = 0; + if (use_national_char_set) { // Insert the letter N if the encoding is UTF8 + *(p + (pos++)) = 'N'; } + *(p + (pos++)) = c; // Add the initial quote + tmp_str.copy(p + pos, len, 0); // Copy tmp_str to *quoted + pos += len; + *(p + pos) = c; // Add the end quote - *quoted_len = unquoted_len + quotes_needed; // length returned to the caller should not account for null terminator. - *quoted = reinterpret_cast( sqlsrv_malloc( *quoted_len, sizeof( char ), 1 )); // include space for null terminator. - unsigned int out_current = 0; - - // insert N if the encoding is UTF8 - if (use_national_char_set) { - ( *quoted )[out_current++] = 'N'; - } - // insert initial quote - ( *quoted )[out_current++] = '\''; - - for ( size_t index = 0; index < unquoted_len; ++index ) { - if ( unquoted[index] == '\'' ) { - ( *quoted )[out_current++] = '\''; - ( *quoted )[out_current++] = '\''; - } - else { - ( *quoted )[out_current++] = unquoted[index]; - } - } - - // trailing quote and null terminator - ( *quoted )[out_current++] = '\''; - ( *quoted )[out_current] = '\0'; - +#if PHP_VERSION_ID < 80100 return 1; +#else + zend_string* zstr = zend_string_init(quoted, quoted_len, 0); + return zstr; +#endif } } @@ -1613,12 +1831,13 @@ pdo_sqlsrv_function_entry *pdo_sqlsrv_get_driver_methods( _Inout_ pdo_dbh_t *dbh PDO_LOG_DBH_ENTRY; sqlsrv_conn* driver_conn = reinterpret_cast( dbh->driver_data ); - SQLSRV_ASSERT( driver_conn != NULL, "pdo_sqlsrv_get_driver_methods: driver_data object was NULL." ); - CHECK_CUSTOM_ERROR( true, driver_conn, PDO_SQLSRV_ERROR_FUNCTION_NOT_IMPLEMENTED ) { - return NULL; - } - return NULL; // to avoid a compiler warning + // As per documentation, simply return false if the method does not exist + // https://www.php.net/manual/en/function.is-callable.php + // But user can call PDO::errorInfo() to check the error message if necessary + CHECK_CUSTOM_WARNING_AS_ERROR(true, driver_conn, PDO_SQLSRV_ERROR_FUNCTION_NOT_IMPLEMENTED); + + return NULL; // return NULL for PDO to take care of the rest } namespace { diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index 427397c7..3d7d2395 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -3,7 +3,7 @@ // // Contents: initialization routines for PDO_SQLSRV // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index 74db829e..6d160893 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -5,7 +5,7 @@ // // Copyright Microsoft Corporation // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 9ecab26a..79bac4dd 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDOStatement object for the PDO_SQLSRV // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -211,15 +211,6 @@ void set_stmt_encoding( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z ) } } -// internal helper function to free meta data structures allocated -void meta_data_free( _Inout_ field_meta_data* meta ) -{ - if( meta->field_name ) { - meta->field_name.reset(); - } - sqlsrv_free( meta ); -} - zval convert_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) { zval out_zval; @@ -287,8 +278,13 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, _Inout_ struct pdo_bound_param_data *param, _In_ enum pdo_param_event event_type ); int pdo_sqlsrv_stmt_describe_col( _Inout_ pdo_stmt_t *stmt, _In_ int colno ); +#if PHP_VERSION_ID < 80100 int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, _Out_writes_bytes_opt_(*len) char **ptr, _Inout_ size_t *len, _Out_opt_ int *caller_frees ); +#else +int pdo_sqlsrv_stmt_get_col_data(_Inout_ pdo_stmt_t *stmt, _In_ int colno, _Inout_ zval *result, _Inout_ enum pdo_param_type *type); +#endif + int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _Inout_ zval *val ); int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _Inout_ zval *return_value ); int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno, _Inout_ zval *return_value ); @@ -324,7 +320,7 @@ void stmt_option_encoding:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option c void stmt_option_direct_query:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) { pdo_sqlsrv_stmt *pdo_stmt = static_cast( stmt ); - pdo_stmt->direct_query = ( zend_is_true( value_z )) ? true : false; + pdo_stmt->direct_query = zend_is_true(value_z); } void stmt_option_cursor_scroll_type:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) @@ -341,13 +337,13 @@ void stmt_option_emulate_prepares:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_ void stmt_option_fetch_numeric:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) { pdo_sqlsrv_stmt *pdo_stmt = static_cast( stmt ); - pdo_stmt->fetch_numeric = ( zend_is_true( value_z )) ? true : false; + pdo_stmt->fetch_numeric = zend_is_true(value_z); } void stmt_option_fetch_datetime:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) { pdo_sqlsrv_stmt *pdo_stmt = static_cast( stmt ); - pdo_stmt->fetch_datetime = ( zend_is_true( value_z )) ? true : false; + pdo_stmt->fetch_datetime = zend_is_true(value_z); } // log a function entry point @@ -362,9 +358,6 @@ void stmt_option_fetch_datetime:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op // PDO SQLSRV statement destructor pdo_sqlsrv_stmt::~pdo_sqlsrv_stmt( void ) { - std::for_each( current_meta_data.begin(), current_meta_data.end(), meta_data_free ); - current_meta_data.clear(); - if( bound_column_param_types ) { sqlsrv_free( bound_column_param_types ); bound_column_param_types = NULL; @@ -465,9 +458,10 @@ int pdo_sqlsrv_stmt_describe_col( _Inout_ pdo_stmt_t *stmt, _In_ int colno) // Set the precision column_data->precision = core_meta_data->field_scale; +#if PHP_VERSION_ID < 80100 // Set the param_type column_data->param_type = PDO_PARAM_ZVAL; - +#endif // store the field data for use by pdo_sqlsrv_stmt_get_col_data pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); SQLSRV_ASSERT( driver_stmt != NULL, "Invalid driver statement in pdo_sqlsrv_stmt_describe_col" ); @@ -563,8 +557,13 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt ) zend_hash_internal_pointer_reset(driver_stmt->placeholders); +#if PHP_VERSION_ID < 80100 query = stmt->active_query_string; query_len = static_cast(stmt->active_query_stringlen); +#else + query = ZSTR_VAL(stmt->active_query_string); + query_len = ZSTR_LEN(stmt->active_query_string); +#endif } // The query timeout setting is inherited from the corresponding connection attribute, but @@ -664,14 +663,23 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta pdo_bound_param_data* bind_data = NULL; if( !driver_stmt->bound_column_param_types ) { - driver_stmt->bound_column_param_types = +#if PHP_VERSION_ID < 80100 + driver_stmt->bound_column_param_types = reinterpret_cast( sqlsrv_malloc( stmt->column_count, sizeof( pdo_param_type ), 0 )); std::fill( driver_stmt->bound_column_param_types, driver_stmt->bound_column_param_types + stmt->column_count, PDO_PARAM_ZVAL ); +#else + // TODO: possibly no longer need bound_column_param_types?? default to PDO_PARAM_STR??? + driver_stmt->bound_column_param_types = + reinterpret_cast(sqlsrv_malloc(stmt->column_count, sizeof(pdo_param_type), 0)); + std::fill(driver_stmt->bound_column_param_types, driver_stmt->bound_column_param_types + stmt->column_count, + PDO_PARAM_STR); +#endif } for( long i = 0; i < stmt->column_count; ++i ) { +#if PHP_VERSION_ID < 80100 if (NULL== (bind_data = reinterpret_cast(zend_hash_index_find_ptr(stmt->bound_columns, i))) && (NULL == (bind_data = reinterpret_cast(zend_hash_find_ptr(stmt->bound_columns, stmt->columns[i].name))))) { @@ -684,6 +692,15 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta driver_stmt->bound_column_param_types[i] = bind_data->param_type; bind_data->param_type = PDO_PARAM_ZVAL; } +#else + if (NULL == (bind_data = reinterpret_cast(zend_hash_index_find_ptr(stmt->bound_columns, i))) && + (NULL == (bind_data = reinterpret_cast(zend_hash_find_ptr(stmt->bound_columns, stmt->columns[i].name))))) { + continue; + } + + // TODO: possibly no longer need bound_column_param_types?? + driver_stmt->bound_column_param_types[i] = bind_data->param_type; +#endif } } @@ -742,8 +759,13 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta // freeing the memory. // Return: // 0 for failure, 1 for success. +#if PHP_VERSION_ID < 80100 int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, _Out_writes_bytes_opt_(*len) char **ptr, _Inout_ size_t *len, _Out_opt_ int *caller_frees) +#else +int pdo_sqlsrv_stmt_get_col_data(_Inout_ pdo_stmt_t *stmt, _In_ int colno, _Inout_ zval *result_z, _Inout_ enum pdo_param_type *type) +#endif + { PDO_RESET_STMT_ERROR; PDO_VALIDATE_STMT; @@ -751,39 +773,44 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, try { - SQLSRV_ASSERT( stmt != NULL, "pdo_sqlsrv_stmt_get_col_data: pdo_stmt object was null" ); + SQLSRV_ASSERT(stmt != NULL, "pdo_sqlsrv_stmt_get_col_data: pdo_stmt object was null"); - pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); - - SQLSRV_ASSERT( driver_stmt != NULL, "pdo_sqlsrv_stmt_get_col_data: driver_data object was null" ); + pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast(stmt->driver_data); - CHECK_CUSTOM_ERROR((colno < 0), driver_stmt, PDO_SQLSRV_ERROR_INVALID_COLUMN_INDEX ) { + SQLSRV_ASSERT(driver_stmt != NULL, "pdo_sqlsrv_stmt_get_col_data: driver_data object was null"); + + CHECK_CUSTOM_ERROR((colno < 0), driver_stmt, PDO_SQLSRV_ERROR_INVALID_COLUMN_INDEX) { return 0; } - + +#if PHP_VERSION_ID < 80100 // Let PDO free the memory after use. - *caller_frees = 1; - + * caller_frees = 1; +#endif + // translate the pdo type to a type the core layer understands sqlsrv_phptype sqlsrv_php_type; - SQLSRV_ASSERT( colno >= 0 && colno < static_cast( driver_stmt->current_meta_data.size()), - "Invalid column number in pdo_sqlsrv_stmt_get_col_data" ); + SQLSRV_ASSERT(colno >= 0 && colno < static_cast(driver_stmt->current_meta_data.size()), + "Invalid column number in pdo_sqlsrv_stmt_get_col_data"); // set the encoding if the user specified one via bindColumn, otherwise use the statement's encoding // save the php type for next use sqlsrv_php_type = driver_stmt->sql_type_to_php_type( - static_cast(driver_stmt->current_meta_data[colno]->field_type), - static_cast(driver_stmt->current_meta_data[colno]->field_size), - true); + static_cast(driver_stmt->current_meta_data[colno]->field_type), + static_cast(driver_stmt->current_meta_data[colno]->field_size), + true); driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; // if a column is bound to a type different than the column type, figure out a way to convert it to the // type they want - if( stmt->bound_columns && driver_stmt->bound_column_param_types[colno] != PDO_PARAM_ZVAL ) { - - sqlsrv_php_type.typeinfo.type = pdo_type_to_sqlsrv_php_type( driver_stmt, - driver_stmt->bound_column_param_types[colno] - ); +#if PHP_VERSION_ID < 80100 + if (stmt->bound_columns && driver_stmt->bound_column_param_types[colno] != PDO_PARAM_ZVAL) { +#else + if (stmt->bound_columns) { +#endif + sqlsrv_php_type.typeinfo.type = pdo_type_to_sqlsrv_php_type(driver_stmt, + driver_stmt->bound_column_param_types[colno] + ); pdo_bound_param_data* bind_data = NULL; bind_data = reinterpret_cast(zend_hash_index_find_ptr(stmt->bound_columns, colno)); @@ -792,40 +819,41 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, bind_data = reinterpret_cast(zend_hash_find_ptr(stmt->bound_columns, stmt->columns[colno].name)); } - if( bind_data != NULL && !Z_ISUNDEF(bind_data->driver_params) ) { + if (bind_data != NULL && !Z_ISUNDEF(bind_data->driver_params)) { - CHECK_CUSTOM_ERROR( Z_TYPE( bind_data->driver_params ) != IS_LONG, driver_stmt, - PDO_SQLSRV_ERROR_INVALID_COLUMN_DRIVER_DATA, colno + 1 ) { + CHECK_CUSTOM_ERROR(Z_TYPE(bind_data->driver_params) != IS_LONG, driver_stmt, + PDO_SQLSRV_ERROR_INVALID_COLUMN_DRIVER_DATA, colno + 1) { throw pdo::PDOException(); } - CHECK_CUSTOM_ERROR( driver_stmt->bound_column_param_types[colno] != PDO_PARAM_STR - && driver_stmt->bound_column_param_types[colno] != PDO_PARAM_LOB, driver_stmt, - PDO_SQLSRV_ERROR_COLUMN_TYPE_DOES_NOT_SUPPORT_ENCODING, colno + 1 ) { + CHECK_CUSTOM_ERROR(driver_stmt->bound_column_param_types[colno] != PDO_PARAM_STR + && driver_stmt->bound_column_param_types[colno] != PDO_PARAM_LOB, driver_stmt, + PDO_SQLSRV_ERROR_COLUMN_TYPE_DOES_NOT_SUPPORT_ENCODING, colno + 1) { - throw pdo::PDOException(); + throw pdo::PDOException(); } - sqlsrv_php_type.typeinfo.encoding = Z_LVAL( bind_data->driver_params ); + sqlsrv_php_type.typeinfo.encoding = Z_LVAL(bind_data->driver_params); - switch( sqlsrv_php_type.typeinfo.encoding ) { - case SQLSRV_ENCODING_SYSTEM: - case SQLSRV_ENCODING_BINARY: - case SQLSRV_ENCODING_UTF8: - break; - default: - THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_DRIVER_COLUMN_ENCODING, colno ); - break; + switch (sqlsrv_php_type.typeinfo.encoding) { + case SQLSRV_ENCODING_SYSTEM: + case SQLSRV_ENCODING_BINARY: + case SQLSRV_ENCODING_UTF8: + break; + default: + THROW_PDO_ERROR(driver_stmt, PDO_SQLSRV_ERROR_INVALID_DRIVER_COLUMN_ENCODING, colno); + break; } } // save the php type for the bound column driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; } - + SQLSRV_PHPTYPE sqlsrv_phptype_out = SQLSRV_PHPTYPE_INVALID; - core_sqlsrv_get_field( driver_stmt, colno, sqlsrv_php_type, false, *(reinterpret_cast(ptr)), - reinterpret_cast( len ), true, &sqlsrv_phptype_out ); +#if PHP_VERSION_ID < 80100 + core_sqlsrv_get_field(driver_stmt, colno, sqlsrv_php_type, false, *(reinterpret_cast(ptr)), + reinterpret_cast(len), true, &sqlsrv_phptype_out); if (ptr) { zval* zval_ptr = reinterpret_cast(sqlsrv_malloc(sizeof(zval))); @@ -833,7 +861,14 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, *ptr = reinterpret_cast(zval_ptr); *len = sizeof(zval); } - +#else + SQLLEN len = 0; + void *ptr = NULL; + core_sqlsrv_get_field(driver_stmt, colno, sqlsrv_php_type, false, ptr, &len, true, &sqlsrv_phptype_out); + if (ptr) { + *result_z = convert_to_zval(driver_stmt, sqlsrv_phptype_out, &ptr, len); + } +#endif return 1; } catch ( core::CoreException& ) { @@ -892,15 +927,15 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: - driver_stmt->fetch_numeric = ( zend_is_true( val )) ? true : false; + driver_stmt->fetch_numeric = zend_is_true(val); break; case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: - driver_stmt->fetch_datetime = ( zend_is_true( val )) ? true : false; + driver_stmt->fetch_datetime = zend_is_true(val); break; case SQLSRV_ATTR_FORMAT_DECIMALS: - driver_stmt->format_decimals = ( zend_is_true( val )) ? true : false; + driver_stmt->format_decimals = zend_is_true(val); break; case SQLSRV_ATTR_DECIMAL_PLACES: @@ -908,7 +943,7 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; case SQLSRV_ATTR_DATA_CLASSIFICATION: - driver_stmt->data_classification = (zend_is_true(val)) ? true : false; + driver_stmt->data_classification = zend_is_true(val); break; default: @@ -1126,9 +1161,15 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno &out_buff_len, &field_type_num ); add_assoc_string( return_value, "table", table_name ); +#if PHP_VERSION_ID < 80100 if( stmt->columns && stmt->columns[colno].param_type == PDO_PARAM_ZVAL ) { add_assoc_long( return_value, "pdo_type", pdo_type ); } +#else + if (stmt->columns) { + add_assoc_long(return_value, "pdo_type", pdo_type); + } +#endif } catch( core::CoreException& ) { zval_ptr_dtor(return_value); @@ -1170,8 +1211,7 @@ int pdo_sqlsrv_stmt_next_rowset( _Inout_ pdo_stmt_t *stmt ) core_sqlsrv_next_result( static_cast( stmt->driver_data ) ); // clear the current meta data since the new result will generate new meta data - std::for_each( driver_stmt->current_meta_data.begin(), driver_stmt->current_meta_data.end(), meta_data_free ); - driver_stmt->current_meta_data.clear(); + driver_stmt->clean_up_results_metadata(); // if there are no more result sets, return that it failed. if( driver_stmt->past_next_result_end == true ) { @@ -1338,8 +1378,19 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, switch( php_out_type ) { case SQLSRV_PHPTYPE_NULL: case SQLSRV_PHPTYPE_STREAM: - THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE ); + { + zval *zv = ¶m->parameter; + if (Z_ISREF_P(zv)) { + ZVAL_DEREF(zv); + } + // Table-valued parameters are input-only + CHECK_CUSTOM_ERROR(Z_TYPE_P(zv) == IS_ARRAY, driver_stmt, SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY) { + throw pdo::PDOException(); + } + // For other types, simply throw the following error + THROW_PDO_ERROR(driver_stmt, PDO_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE); break; + } case SQLSRV_PHPTYPE_INT: column_size = SQLSRV_UNKNOWN_SIZE; break; @@ -1404,7 +1455,7 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, // and bind the parameter core_sqlsrv_bind_param( driver_stmt, static_cast( param->paramno ), direction, &(param->parameter) , php_out_type, encoding, - sql_type, column_size, decimal_digits ); + sql_type, column_size, decimal_digits); } break; // undo any work done by the core layer after the statement is executed @@ -1412,15 +1463,8 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, { PDO_VALIDATE_STMT; PDO_LOG_STMT_ENTRY; - - // skip column bindings - if( !param->is_param ) { - break; - } - - core_sqlsrv_post_param( reinterpret_cast( stmt->driver_data ), param->paramno, - &(param->parameter) ); } + break; case PDO_PARAM_EVT_FETCH_PRE: break; diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index bab79c99..cb0b0620 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -3,7 +3,7 @@ // // Contents: Utility functions used by both connection or statement functions // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -383,7 +383,7 @@ pdo_error PDO_ERRORS[] = { }, { SQLSRV_ERROR_CE_DRIVER_REQUIRED, - { IMSSP, (SQLCHAR*) "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server.", -78, false } + { IMSSP, (SQLCHAR*) "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server (or above) for %1!s!.", -78, true } }, { SQLSRV_ERROR_CONNECT_INVALID_DRIVER, @@ -461,6 +461,42 @@ pdo_error PDO_ERRORS[] = { PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID, { IMSSP, (SQLCHAR*) "Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.", -97, false} }, + { + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + { IMSSP, (SQLCHAR*) "An error occurred translating a string for Table-Valued Param %1!d! Column %2!d! to UTF-16: %3!s!", -98, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + { IMSSP, (SQLCHAR*) "An invalid type for Table-Valued Param %1!d! Column %2!d! was specified", -99, true } + }, + { + SQLSRV_ERROR_TVP_FETCH_METADATA, + { IMSSP, (SQLCHAR*) "Failed to get metadata for Table-Valued Param %1!d!", -100, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_INPUTS, + { IMSSP, (SQLCHAR*) "Invalid inputs for Table-Valued Param %1!d!", -101, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + { IMSSP, (SQLCHAR*) "Expect a non-empty string for a Type Name for Table-Valued Param %1!d!", -102, true } + }, + { + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + { IMSSP, (SQLCHAR*) "For Table-Valued Param %1!d! the number of values in a row is expected to be %2!d!", -103, true } + }, + { + SQLSRV_ERROR_TVP_STRING_KEYS, + { IMSSP, (SQLCHAR*) "Associative arrays not allowed for Table-Valued Param %1!d!", -104, true } + }, + { + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + { IMSSP, (SQLCHAR*) "Expect an array for each row for Table-Valued Param %1!d!", -105, true } + }, + { + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, + { IMSSP, (SQLCHAR*) "You cannot return data in a table-valued parameter. Table-valued parameters are input-only.", -106, false } + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 6418cb60..923eb20d 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Declarations for the extension // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 68853ca3..7e472744 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -6,7 +6,7 @@ // // Contents: Internal declarations for the extension // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/template.rc b/source/pdo_sqlsrv/template.rc index 561a65df..82ca2f1e 100644 --- a/source/pdo_sqlsrv/template.rc +++ b/source/pdo_sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.cpp b/source/shared/FormattedPrint.cpp index 23afab35..972aa243 100644 --- a/source/shared/FormattedPrint.cpp +++ b/source/shared/FormattedPrint.cpp @@ -6,7 +6,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.h b/source/shared/FormattedPrint.h index 1d853b15..abf89395 100644 --- a/source/shared/FormattedPrint.h +++ b/source/shared/FormattedPrint.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.cpp b/source/shared/StringFunctions.cpp index 7b08fcf6..ff9d9e58 100644 --- a/source/shared/StringFunctions.cpp +++ b/source/shared/StringFunctions.cpp @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.h b/source/shared/StringFunctions.h index b7850954..2af4afab 100644 --- a/source/shared/StringFunctions.h +++ b/source/shared/StringFunctions.h @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 293cc790..f4733fe6 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use connection handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -48,9 +48,6 @@ const int INFO_BUFFER_LEN = 256; // length for name of keystore used in CEKeyStoreData const int MAX_CE_NAME_LEN = 260; -// processor architectures -const char* PROCESSOR_ARCH[] = { "x86", "x64", "ia64" }; - // ODBC driver names. // the order of this list should match the order of DRIVER_VERSION enum std::vector CONNECTION_STRING_DRIVER_NAME{ "Driver={ODBC Driver 17 for SQL Server};", "Driver={ODBC Driver 13 for SQL Server};", "Driver={ODBC Driver 11 for SQL Server};" }; @@ -561,18 +558,16 @@ void core_sqlsrv_prepare( _Inout_ sqlsrv_stmt* stmt, _In_reads_bytes_(sql_len) c // prepare our wide char query string core::SQLPrepareW( stmt, reinterpret_cast( wsql_string.get() ), wsql_len ); - stmt->param_descriptions.clear(); - // if AE is enabled, get meta data for all parameters before binding them if( stmt->conn->ce_option.enabled ) { SQLSMALLINT num_params; core::SQLNumParams( stmt, &num_params); + for( int i = 0; i < num_params; i++ ) { param_meta_data param; + core::SQLDescribeParam(stmt, i + 1, &(param.sql_type), &(param.column_size), &(param.decimal_digits), &(param.nullable)); - core::SQLDescribeParam( stmt, i + 1, &( param.sql_type ), &( param.column_size ), &( param.decimal_digits ), &( param.nullable ) ); - - stmt->param_descriptions.push_back( param ); + stmt->params_container.params_meta_ae.push_back(param); } } } @@ -919,45 +914,30 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou // and return the string of the processor name. const char* get_processor_arch( void ) { -#ifndef _WIN32 - struct utsname sys_info; - if ( uname(&sys_info) == -1 ) - { - DIE( "Error retrieving system info" ); - } - if( strcmp(sys_info.machine, "x86") == 0 ) { - return PROCESSOR_ARCH[0]; - } else if ( strcmp(sys_info.machine, "x86_64") == 0) { - return PROCESSOR_ARCH[1]; - } else if ( strcmp(sys_info.machine, "ia64") == 0 ) { - return PROCESSOR_ARCH[2]; - } else { - DIE( "Unknown processor architecture." ); - } - return NULL; -#else + // processor architectures + const char* PROCESSOR_ARCH[] = {"x86", "x64", "arm64"}; +#ifdef _WIN32 SYSTEM_INFO sys_info; - GetSystemInfo( &sys_info); - switch( sys_info.wProcessorArchitecture ) { - - case PROCESSOR_ARCHITECTURE_INTEL: - return PROCESSOR_ARCH[0]; - - case PROCESSOR_ARCHITECTURE_AMD64: - return PROCESSOR_ARCH[1]; - - case PROCESSOR_ARCHITECTURE_IA64: - return PROCESSOR_ARCH[2]; - - default: - DIE( "Unknown Windows processor architecture." ); - return NULL; + GetSystemInfo(&sys_info); + switch (sys_info.wProcessorArchitecture) { + case PROCESSOR_ARCHITECTURE_INTEL: + return PROCESSOR_ARCH[0]; + case PROCESSOR_ARCHITECTURE_AMD64: + return PROCESSOR_ARCH[1]; + default: + DIE("Unsupported Windows processor architecture."); + return NULL; } +#elif defined(__arm64__) + return PROCESSOR_ARCH[2]; +#elif defined(__x86_64__) + return PROCESSOR_ARCH[1]; +#else + DIE("Unsupported processor architecture."); return NULL; -#endif // !_WIN32 +#endif // _WIN32 } - // some features require a server of a certain version or later // this function determines the version of the server we're connected to // and stores it in the connection. Any errors are logged before return. @@ -1010,8 +990,8 @@ void load_azure_key_vault(_Inout_ sqlsrv_conn* conn) char *akv_id = conn->ce_option.akv_id.get(); char *akv_secret = conn->ce_option.akv_secret.get(); - unsigned int id_len = strnlen_s(akv_id); - unsigned int key_size = strnlen_s(akv_secret); + size_t id_len = strnlen_s(akv_id); + size_t key_size = strnlen_s(akv_secret); configure_azure_key_vault(conn, AKV_CONFIG_FLAGS, conn->ce_option.akv_mode, 0); configure_azure_key_vault(conn, AKV_CONFIG_PRINCIPALID, akv_id, id_len); diff --git a/source/shared/core_init.cpp b/source/shared/core_init.cpp index 0cd00407..b42e4e34 100644 --- a/source/shared/core_init.cpp +++ b/source/shared/core_init.cpp @@ -3,7 +3,7 @@ // // Contents: common initialization routines shared by PDO and sqlsrv // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index bdc9f6fb..3b8a9d3a 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -3,7 +3,7 @@ // // Contents: Result sets // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 3a3a8af5..8209bf3d 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Core routines and constants shared by the Microsoft Drivers for PHP for SQL Server // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -207,6 +207,7 @@ enum SQLSRV_PHPTYPE { SQLSRV_PHPTYPE_STRING, SQLSRV_PHPTYPE_DATETIME, SQLSRV_PHPTYPE_STREAM, + SQLSRV_PHPTYPE_TABLE, MAX_SQLSRV_PHPTYPE, // highest value for a php type SQLSRV_PHPTYPE_INVALID = MAX_SQLSRV_PHPTYPE // used to see if a type is invalid }; @@ -1394,51 +1395,185 @@ struct param_meta_data SQLULEN get_column_size() { return column_size; } }; -// holds the output parameter information. Strings also need the encoding and other information for -// after processing. Only integer, float, and strings are allowable output parameters. -struct sqlsrv_output_param { - - zval* param_z; +// *** parameter struct used for SQLBindParameter *** +struct sqlsrv_param +{ + SQLUSMALLINT param_pos; // 0-based - the position in the parameters of the statement + SQLSMALLINT direction; + SQLSMALLINT c_data_type; + SQLSMALLINT sql_data_type; + SQLULEN column_size; + SQLSMALLINT decimal_digits; + SQLPOINTER buffer; + SQLLEN buffer_length; + SQLLEN strlen_or_indptr; + int param_php_type; SQLSRV_ENCODING encoding; - SQLUSMALLINT param_num; // used to index into the ind_or_len of the statement - SQLLEN original_buffer_len; // used to make sure the returned length didn't overflow the buffer - SQLSRV_PHPTYPE php_out_type; // used to convert output param if necessary - bool is_bool; - param_meta_data meta_data; // parameter meta data + bool was_null; // false by default - the original parameter was a NULL zval + zval placeholder_z; // A temp zval for binding any input parameter value, including simple data types, wide input string (UTF-16 buffer), the datetime strings, etc. + zval* param_ptr_z; // NULL by default - points to the original parameter or its reference + std::size_t num_bytes_read; // 0 by default - number of bytes processed so far (for an empty PHP stream, an empty string is sent to the server) + php_stream* param_stream; // NULL by default - used to send stream data from an input parameter to the server + + sqlsrv_param(_In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT dir, _In_ SQLSRV_ENCODING enc, _In_ SQLSMALLINT sql_type, _In_ SQLULEN col_size, _In_ SQLSMALLINT dec_digits) : + c_data_type(0), buffer(NULL), buffer_length(0), strlen_or_indptr(0), param_pos(param_num), direction(dir), encoding(enc), sql_data_type(sql_type), + column_size(col_size), decimal_digits(dec_digits), param_php_type(0), was_null(false), param_ptr_z(NULL), num_bytes_read(0), param_stream(NULL) + { + ZVAL_UNDEF(&placeholder_z); + } - // string output param constructor - sqlsrv_output_param( _In_ zval* p_z, _In_ SQLSRV_ENCODING enc, _In_ int num, _In_ SQLUINTEGER buffer_len ) : - param_z(p_z), encoding(enc), param_num(num), original_buffer_len(buffer_len), is_bool(false), php_out_type(SQLSRV_PHPTYPE_INVALID) + void copy_param_meta_ae(_Inout_ zval* param_z, _In_ param_meta_data& meta); // Only used when Always Encrypted is enabled + + virtual ~sqlsrv_param(){ release_data(); } + virtual void release_data(); + + bool derive_string_types_sizes(_In_ zval* param_z); + bool preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + + // The following methods change the member placeholder_z, and both will return false if conversions fail + bool convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + bool convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + + virtual bool prepare_param(_In_ zval* param_ref, _Inout_ zval* param_z); + virtual void process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + virtual void process_null_param(_Inout_ zval* param_z); + virtual void process_bool_param(_Inout_ zval* param_z); + virtual void process_long_param(_Inout_ zval* param_z); + virtual void process_double_param(_Inout_ zval* param_z); + virtual void process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + virtual void process_resource_param(_Inout_ zval* param_z); + virtual void process_object_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + + virtual void bind_param(_Inout_ sqlsrv_stmt* stmt); + + // The following methods are used to supply data to the server via SQLPutData + virtual void init_data_from_zval(_Inout_ sqlsrv_stmt* stmt); + virtual bool send_data_packet(_Inout_ sqlsrv_stmt* stmt); +}; + +// *** output / inout parameter struct used for SQLBindParameter, inheriting sqlsrv_param *** +struct sqlsrv_param_inout : public sqlsrv_param +{ + SQLSRV_PHPTYPE php_out_type; // Used to convert output param when necessary + bool was_bool; // false by default - the original parameter was a boolean zval + sqlsrv_stmt* stmt; // NULL by default - points to the statement object mainly for error processing + + sqlsrv_param_inout(_In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT dir, _In_ SQLSRV_ENCODING enc, _In_ SQLSMALLINT sql_type, + _In_ SQLULEN col_size, _In_ SQLSMALLINT dec_digits, SQLSRV_PHPTYPE php_out_type) : + sqlsrv_param(param_num, dir, enc, sql_type, col_size, dec_digits), + php_out_type(php_out_type), was_bool(false), stmt(NULL) { } - // every other type output parameter constructor - sqlsrv_output_param( _In_ zval* p_z, _In_ int num, _In_ bool is_bool, _In_ SQLSRV_PHPTYPE php_out_type) : - param_z( p_z ), - encoding( SQLSRV_ENCODING_INVALID ), - param_num( num ), - original_buffer_len( -1 ), - is_bool( is_bool ), - php_out_type(php_out_type) + virtual ~sqlsrv_param_inout() { param_ptr_z = NULL; } + virtual void release_data() { param_ptr_z = NULL; } + + virtual bool prepare_param(_In_ zval* param_ref, _Inout_ zval* param_z); + virtual void process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + virtual void process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + + // Called when the output parameter is ready to be finalized, using the value stored in param_ptr_z + void finalize_output_value(); + + // Resize the output string buffer based on its properties and whether it is a numeric type + void resize_output_string_buffer(_Inout_ zval* param_z, _In_ bool is_numeric_type); + + // A helper method called by finalize_output_value() to finalize output string parameters + void finalize_output_string(); +}; + +// *** Table-valued parameter struct used for SQLBindParameter, inheriting sqlsrv_param +// *** A sqlsrv_param_tvp can be representing a table-valued parameter itself or one of +// *** its constituent columns. When it is a table-valued parameter, tvp_columns cannot +// *** be empty. When it is a TVP column, parent_tvp points to its table-valued parameter +// *** and tvp_columns must be empty. The member param_pos refers to the ordinal position +// *** of this column in the corresponding table type. +struct sqlsrv_param_tvp : public sqlsrv_param +{ + std::map tvp_columns; // The constituent columns of the table-valued parameter + + sqlsrv_param_tvp* parent_tvp; // For a TVP column to reference to the table-valued parameter. NULL if this is the TVP itself. + int num_rows; // The total number of rows + int current_row; // A counter to keep track of which row is to be processed + + sqlsrv_param_tvp(_In_ SQLUSMALLINT param_num, _In_ SQLSRV_ENCODING enc, _In_ SQLSMALLINT sql_type, _In_ SQLULEN col_size, _In_ SQLSMALLINT dec_digits, _In_ sqlsrv_param_tvp* tvp) : + sqlsrv_param(param_num, SQL_PARAM_INPUT, enc, sql_type, col_size, dec_digits), num_rows(0), current_row(0), parent_tvp(tvp) { + ZVAL_UNDEF(&placeholder_z); + } + virtual ~sqlsrv_param_tvp() { release_data(); } + virtual void release_data(); + virtual void bind_param(_Inout_ sqlsrv_stmt* stmt); + virtual void process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + + // The following methods are used to supply data to the server post execution + virtual void init_data_from_zval(_Inout_ sqlsrv_stmt* stmt) {} + virtual bool send_data_packet(_Inout_ sqlsrv_stmt* stmt); + + // Change the column encoding based on the sql data type + static void sql_type_to_encoding(_In_ SQLSMALLINT sql_type, _Inout_ SQLSRV_ENCODING* encoding); + + // The following methods are only applicable to a table-valued parameter or its individual columns + int parse_tv_param_arrays(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + void get_tvp_metadata(_In_ sqlsrv_stmt* stmt, _In_ zend_string* table_type_name, _In_ zend_string* schema_name); + void process_param_column_value(_Inout_ sqlsrv_stmt* stmt); + void process_null_param_value(_Inout_ sqlsrv_stmt* stmt); + void populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_ int ordinal); + void send_string_data_in_batches(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z); +}; + +// *** a container of all parameters used for SQLBindParameter *** +struct sqlsrv_params_container +{ + std::vector params_meta_ae; // Empty by default - only used when Always Encrypted is enabled + + std::map input_params; // map of pointers to the input params with their ordinal positions as keys + std::map output_params; // map of pointers to the output / inout params with their ordinal positions as keys + + sqlsrv_param* current_param; // Null by default - points to a sqlsrv_param object used for sending stream data + + sqlsrv_params_container() { current_param = NULL; } + ~sqlsrv_params_container() { params_meta_ae.clear(); clean_up_param_data(); } + + sqlsrv_param* find_param(_In_ SQLUSMALLINT param_num, _In_ bool is_input); + void insert_param(_In_ SQLUSMALLINT param_num, _In_ sqlsrv_param* new_param) + { + if (new_param->direction == SQL_PARAM_INPUT) { + input_params[param_num] = new_param; + } else { + output_params[param_num] = new_param; + } } - void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE) + void remove_params(std::map& params_map) { - meta_data.sql_type = sql_type; - meta_data.column_size = column_size; - meta_data.decimal_digits = decimal_digits; - meta_data.nullable = nullable; + std::map::iterator it1; + for (it1 = params_map.begin(); it1 != params_map.end(); ++it1) { + sqlsrv_param* ptr = it1->second; + if (ptr) { + ptr->release_data(); + sqlsrv_free(ptr); + } + } + params_map.clear(); } - param_meta_data& getMetaData() + void clean_up_param_data(_In_opt_ bool only_input = false); + void finalize_output_parameters(); + + // The following functions are used to supply data to the server post execution + bool get_next_parameter(_Inout_ sqlsrv_stmt* stmt); + bool send_next_packet(_Inout_ sqlsrv_stmt* stmt); + void send_all_packets(_Inout_ sqlsrv_stmt* stmt) { - return meta_data; + while (get_next_parameter(stmt)) { + while (current_param->send_data_packet(stmt)) {} + } } }; namespace data_classification { - const int VERSION_RANK_AVAILABLE = 2; // Rank info is available when data classification version is 2+ + const size_t VERSION_RANK_AVAILABLE = 2; // Rank info is available when data classification version is 2+ const int RANK_NOT_DEFINED = -1; // *** data classficiation metadata structures and helper methods -- to store and/or process the sensitivity classification data *** struct name_id_pair; @@ -1522,6 +1657,10 @@ struct sqlsrv_stmt : public sqlsrv_context { // free sensitivity classification metadata void clean_up_sensitivity_metadata(); + + // free resultset metadata + void clean_up_results_metadata(); + // set query timeout void set_query_timeout(); @@ -1544,23 +1683,12 @@ struct sqlsrv_stmt : public sqlsrv_context { short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits) bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata - // holds output pointers for SQLBindParameter - // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving - // memory, which is important because we pass the pointer to an element of the deque to SQLBindParameter to hold - std::deque param_ind_ptrs; // output pointers for lengths for calls to SQLBindParameter - zval param_input_strings; // hold all UTF-16 input strings that aren't managed by PHP - zval output_params; // hold all the output parameters - zval param_streams; // track which streams to send data to the server - zval param_datetime_buffers; // datetime strings to be converted back to DateTime objects bool send_streams_at_exec; // send all stream data right after execution before returning - sqlsrv_stream current_stream; // current stream sending data to the server as an input parameter - unsigned int current_stream_read; // # of bytes read so far. (if we read an empty PHP stream, we send an empty string - // to the server) zval field_cache; // cache for a single row of fields, to allow multiple and out of order retrievals zval col_cache; // Used by get_field_as_string not to call SQLColAttribute() after every fetch. zval active_stream; // the currently active stream reading data from the database - std::vector param_descriptions; + sqlsrv_params_container params_container; // holds all parameters and references used for SQLBindParameter // meta data for current result set std::vector> current_meta_data; @@ -1630,7 +1758,7 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm _In_opt_ const stmt_option valid_stmt_opts[], _In_ error_callback const err, _In_opt_ void* driver ); void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT direction, _Inout_ zval* param_z, _In_ SQLSRV_PHPTYPE php_out_type, _Inout_ SQLSRV_ENCODING encoding, _Inout_ SQLSMALLINT sql_type, _Inout_ SQLULEN column_size, - _Inout_ SQLSMALLINT decimal_digits ); + _Inout_ SQLSMALLINT decimal_digits); SQLRETURN core_sqlsrv_execute( _Inout_ sqlsrv_stmt* stmt, _In_reads_bytes_(sql_len) const char* sql = NULL, _In_ int sql_len = 0 ); field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno ); bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orientation, _In_ SQLULEN fetch_offset ); @@ -1639,11 +1767,9 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i _Out_ SQLSRV_PHPTYPE *sqlsrv_php_type_out); bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt ); void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_output_params = true, _In_ bool throw_on_errors = true ); -void core_sqlsrv_post_param( _Inout_ sqlsrv_stmt* stmt, _In_ zend_ulong paramno, zval* param_z ); void core_sqlsrv_set_scrollable( _Inout_ sqlsrv_stmt* stmt, _In_ unsigned long cursor_type ); void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z ); -void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z ); -bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt ); +bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt, _In_opt_ bool get_all = false); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit ); void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z); @@ -1889,6 +2015,15 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + SQLSRV_ERROR_TVP_FETCH_METADATA, + SQLSRV_ERROR_TVP_INVALID_INPUTS, + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + SQLSRV_ERROR_TVP_STRING_KEYS, + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index d87231f8..400ac5c1 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use statement handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -94,42 +94,22 @@ const size_t DATE_FORMAT_LEN = sizeof( DATE_FORMAT ); } // *** internal functions *** -// Only declarations are put here. Functions contain the documentation they need at their definition sites. +// Only declarations are put here. Functions contain more explanations they need in their definitions void calc_string_size( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLLEN sql_type, _Inout_ SQLLEN& size ); size_t calc_utf8_missing( _Inout_ sqlsrv_stmt* stmt, _In_reads_(buffer_end) const char* buffer, _In_ size_t buffer_end ); -bool check_for_next_stream_parameter( _Inout_ sqlsrv_stmt* stmt ); -bool convert_input_param_to_utf16( _In_ zval* input_param_z, _Inout_ zval* convert_param_z ); void core_get_field_common(_Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type, _Inout_updates_bytes_(*field_len) void*& field_value, _Inout_ SQLLEN* field_len); -// returns the ODBC C type constant that matches the PHP type and encoding given -SQLSMALLINT default_c_type(_Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_ zval const* param_z, _In_ SQLSMALLINT sql_type, _In_ SQLSRV_ENCODING encoding); -void default_sql_size_and_scale( _Inout_ sqlsrv_stmt* stmt, _In_opt_ unsigned int paramno, _In_ zval* param_z, _In_ SQLSRV_ENCODING encoding, - _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits ); -// given a zval and encoding, determine the appropriate sql type, column size, and decimal scale (if appropriate) -void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_ zval* param_z, _In_ SQLSRV_ENCODING encoding, - _Out_ SQLSMALLINT& sql_type ); void col_cache_dtor( _Inout_ zval* data_z ); void field_cache_dtor( _Inout_ zval* data_z ); int round_up_decimal_numbers(_Inout_ char* buffer, _In_ int decimal_pos, _In_ int decimals_places, _In_ int offset, _In_ int lastpos); void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT field_scale, _Inout_updates_bytes_(*field_len) char*& field_value, _Inout_ SQLLEN* field_len); -void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt, _In_opt_ bool exception_thrown = false ); void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type, _Inout_updates_bytes_(*field_len) void*& field_value, _Inout_ SQLLEN* field_len ); stmt_option const* get_stmt_option( sqlsrv_conn const* conn, _In_ zend_ulong key, _In_ const stmt_option stmt_opts[] ); bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ); -// assure there is enough space for the output parameter string -void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z, _In_ SQLULEN paramno, SQLSRV_ENCODING encoding, - _In_ SQLSMALLINT c_type, _In_ SQLSMALLINT sql_type, _In_ SQLULEN column_size, _In_ SQLSMALLINT decimal_digits, - _Out_writes_(buffer_len) SQLPOINTER& buffer, _Out_ SQLLEN& buffer_len ); void adjustDecimalPrecision(_Inout_ zval* param_z, _In_ SQLSMALLINT decimal_digits); -void save_output_param_for_later( _Inout_ sqlsrv_stmt* stmt, _Inout_ sqlsrv_output_param& param ); -// send all the stream data -void send_param_streams( _Inout_ sqlsrv_stmt* stmt ); -// called when a bound output string parameter is to be destroyed -void sqlsrv_output_param_dtor( _Inout_ zval* data ); -// called when a bound stream parameter is to be destroyed. -void sqlsrv_stream_dtor( _Inout_ zval* data ); bool is_a_numeric_type(_In_ SQLSMALLINT sql_type); +bool is_a_string_type(_In_ SQLSMALLINT sql_type); } // constructor for sqlsrv_stmt. Here so that we can use functions declared earlier. @@ -152,32 +132,16 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error decimal_places(NO_CHANGE_DECIMAL_PLACES), // the default is no formatting to resultset required data_classification(false), buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ), - param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte - send_streams_at_exec( true ), - current_stream( NULL, SQLSRV_ENCODING_DEFAULT ), - current_stream_read( 0 ) + send_streams_at_exec( true ) { ZVAL_UNDEF( &active_stream ); - // initialize the input string parameters array (which holds zvals) - array_init(¶m_input_strings); - - // initialize the (input only) stream parameters (which holds sqlsrv_stream structures) - ZVAL_NEW_ARR( ¶m_streams ); - core::sqlsrv_zend_hash_init(*conn, Z_ARRVAL( param_streams ), 5 /* # of buckets */, sqlsrv_stream_dtor, 0 /*persistent*/); - - // initialize the (input only) datetime parameters of converted date time objects to strings - array_init( ¶m_datetime_buffers ); - - // initialize the output string parameters (which holds sqlsrv_output_param structures) - ZVAL_NEW_ARR( &output_params ); - core::sqlsrv_zend_hash_init(*conn, Z_ARRVAL( output_params ), 5 /* # of buckets */, sqlsrv_output_param_dtor, 0 /*persistent*/); // initialize the col cache - ZVAL_NEW_ARR( &col_cache ); + array_init(&col_cache); core::sqlsrv_zend_hash_init( *conn, Z_ARRVAL(col_cache), 5 /* # of buckets */, col_cache_dtor, 0 /*persistent*/ ); // initialize the field cache - ZVAL_NEW_ARR( &field_cache ); + array_init(&field_cache); core::sqlsrv_zend_hash_init(*conn, Z_ARRVAL(field_cache), 5 /* # of buckets */, field_cache_dtor, 0 /*persistent*/); } @@ -198,11 +162,10 @@ sqlsrv_stmt::~sqlsrv_stmt( void ) // delete sensivity data clean_up_sensitivity_metadata(); + // clean up metadata + clean_up_results_metadata(); + invalidate(); - zval_ptr_dtor( ¶m_input_strings ); - zval_ptr_dtor( &output_params ); - zval_ptr_dtor( ¶m_streams ); - zval_ptr_dtor( ¶m_datetime_buffers ); zval_ptr_dtor( &col_cache ); zval_ptr_dtor( &field_cache ); } @@ -213,12 +176,8 @@ sqlsrv_stmt::~sqlsrv_stmt( void ) // execution phase. void sqlsrv_stmt::free_param_data( void ) { - SQLSRV_ASSERT(Z_TYPE( param_input_strings ) == IS_ARRAY && Z_TYPE( param_streams ) == IS_ARRAY, - "sqlsrv_stmt::free_param_data: Param zvals aren't arrays." ); - zend_hash_clean( Z_ARRVAL( param_input_strings )); - zend_hash_clean( Z_ARRVAL( output_params )); - zend_hash_clean( Z_ARRVAL( param_streams )); - zend_hash_clean( Z_ARRVAL( param_datetime_buffers )); + params_container.clean_up_param_data(); + zend_hash_clean( Z_ARRVAL( col_cache )); zend_hash_clean( Z_ARRVAL( field_cache )); } @@ -275,6 +234,22 @@ void sqlsrv_stmt::clean_up_sensitivity_metadata() } } +// internal helper function to free meta data structures allocated +void meta_data_free(_Inout_ field_meta_data* meta) +{ + meta->field_name.reset(); + sqlsrv_free(meta); +} + +void sqlsrv_stmt::clean_up_results_metadata() +{ + std::for_each(current_meta_data.begin(), current_meta_data.end(), meta_data_free); + current_meta_data.clear(); + + column_count = ACTIVE_NUM_COLS_INVALID; + row_count = ACTIVE_NUM_ROWS_INVALID; +} + void sqlsrv_stmt::set_query_timeout() { if (query_timeout == QUERY_TIMEOUT_INVALID || query_timeout < 0) { @@ -371,7 +346,6 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm return return_stmt; } - // core_sqlsrv_bind_param // Binds a parameter using SQLBindParameter. It allocates memory and handles other details // in translating between the driver and ODBC. @@ -389,380 +363,74 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT direction, _Inout_ zval* param_z, _In_ SQLSRV_PHPTYPE php_out_type, _Inout_ SQLSRV_ENCODING encoding, _Inout_ SQLSMALLINT sql_type, _Inout_ SQLULEN column_size, - _Inout_ SQLSMALLINT decimal_digits ) + _Inout_ SQLSMALLINT decimal_digits) { - SQLSMALLINT c_type; - SQLPOINTER buffer = NULL; - SQLLEN buffer_len = 0; - - SQLSRV_ASSERT( direction == SQL_PARAM_INPUT || direction == SQL_PARAM_OUTPUT || direction == SQL_PARAM_INPUT_OUTPUT, - "core_sqlsrv_bind_param: Invalid parameter direction." ); - SQLSRV_ASSERT( direction == SQL_PARAM_INPUT || php_out_type != SQLSRV_PHPTYPE_INVALID, - "core_sqlsrv_bind_param: php_out_type not set before calling core_sqlsrv_bind_param." ); - - try { - // check is only < because params are 0 based - CHECK_CUSTOM_ERROR( param_num >= SQL_SERVER_MAX_PARAMS, stmt, SQLSRV_ERROR_MAX_PARAMS_EXCEEDED, param_num + 1 ){ + CHECK_CUSTOM_ERROR(param_num >= SQL_SERVER_MAX_PARAMS, stmt, SQLSRV_ERROR_MAX_PARAMS_EXCEEDED, param_num + 1) { throw core::CoreException(); } - // resize the statements array of int_ptrs if the parameter isn't already set. - if( stmt->param_ind_ptrs.size() < static_cast( param_num + 1 )){ - stmt->param_ind_ptrs.resize( param_num + 1, SQL_NULL_DATA ); - } - SQLLEN& ind_ptr = stmt->param_ind_ptrs[param_num]; - + // Dereference the parameter if necessary zval* param_ref = param_z; - if( Z_ISREF_P( param_z )){ - ZVAL_DEREF( param_z ); + if (Z_ISREF_P(param_z)) { + ZVAL_DEREF(param_z); } - bool zval_was_null = ( Z_TYPE_P( param_z ) == IS_NULL ); - bool zval_was_bool = ( Z_TYPE_P( param_z ) == IS_TRUE || Z_TYPE_P( param_z ) == IS_FALSE ); - // if the user asks for for a specific type for input and output, make sure the data type we send matches the data we - // type we expect back, since we can only send and receive the same type. Anything can be converted to a string, so - // we always let that match if they want a string back. - if( direction == SQL_PARAM_INPUT_OUTPUT ) { - bool match = false; - switch( php_out_type ){ - case SQLSRV_PHPTYPE_INT: - if( zval_was_null || zval_was_bool ){ - convert_to_long( param_z ); + + sqlsrv_param* param_ptr = stmt->params_container.find_param(param_num, (direction == SQL_PARAM_INPUT)); + try { + if (param_ptr == NULL) { + sqlsrv_malloc_auto_ptr new_param; + if (direction == SQL_PARAM_INPUT) { + // Check if it's a Table-Valued Parameter first + if (Z_TYPE_P(param_z) == IS_ARRAY) { + new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param_tvp))) sqlsrv_param_tvp(param_num, encoding, SQL_SS_TABLE, 0, 0, NULL); + } else { + new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param))) sqlsrv_param(param_num, direction, encoding, sql_type, column_size, decimal_digits); } - match = Z_TYPE_P( param_z ) == IS_LONG; - break; - case SQLSRV_PHPTYPE_FLOAT: - if( zval_was_null ){ - convert_to_double( param_z ); - } - match = Z_TYPE_P( param_z ) == IS_DOUBLE; - break; - case SQLSRV_PHPTYPE_STRING: - // anything can be converted to a string - convert_to_string( param_z ); - match = true; - break; - case SQLSRV_PHPTYPE_NULL: - case SQLSRV_PHPTYPE_DATETIME: - case SQLSRV_PHPTYPE_STREAM: - SQLSRV_ASSERT( false, "Invalid type for an output parameter." ); - break; - default: - SQLSRV_ASSERT( false, "Unknown SQLSRV_PHPTYPE_* constant given." ); - break; - } - CHECK_CUSTOM_ERROR( !match, stmt, SQLSRV_ERROR_INPUT_OUTPUT_PARAM_TYPE_MATCH, param_num + 1 ){ - throw core::CoreException(); - } - } - - // If the user specifies a certain type for an output parameter, we have to convert the zval - // to that type so that when the buffer is filled, the type is correct. But first, - // should check if a LOB type is specified. - CHECK_CUSTOM_ERROR( direction != SQL_PARAM_INPUT && ( sql_type == SQL_LONGVARCHAR - || sql_type == SQL_WLONGVARCHAR || sql_type == SQL_LONGVARBINARY ), - stmt, SQLSRV_ERROR_OUTPUT_PARAM_TYPES_NOT_SUPPORTED ){ - throw core::CoreException(); - } - - if( direction == SQL_PARAM_OUTPUT ){ - switch( php_out_type ) { - case SQLSRV_PHPTYPE_INT: - convert_to_long( param_z ); - break; - case SQLSRV_PHPTYPE_FLOAT: - convert_to_double( param_z ); - break; - case SQLSRV_PHPTYPE_STRING: - convert_to_string( param_z ); - break; - case SQLSRV_PHPTYPE_NULL: - case SQLSRV_PHPTYPE_DATETIME: - case SQLSRV_PHPTYPE_STREAM: - SQLSRV_ASSERT( false, "Invalid type for an output parameter" ); - break; - default: - SQLSRV_ASSERT( false, "Uknown SQLSRV_PHPTYPE_* constant given" ); - break; - } - } - - SQLSRV_ASSERT(( Z_TYPE_P( param_z ) != IS_STRING && Z_TYPE_P( param_z ) != IS_RESOURCE ) || - ( encoding == SQLSRV_ENCODING_SYSTEM || encoding == SQLSRV_ENCODING_UTF8 || - encoding == SQLSRV_ENCODING_BINARY ), "core_sqlsrv_bind_param: invalid encoding" ); - - if( stmt->conn->ce_option.enabled && ( sql_type == SQL_UNKNOWN_TYPE || column_size == SQLSRV_UNKNOWN_SIZE )){ - // use the meta data only if the user has not specified the sql type or column size - SQLSRV_ASSERT( param_num < stmt->param_descriptions.size(), "Invalid param_num passed in core_sqlsrv_bind_param!" ); - sql_type = stmt->param_descriptions[param_num].get_sql_type(); - column_size = stmt->param_descriptions[param_num].get_column_size(); - decimal_digits = stmt->param_descriptions[param_num].get_decimal_digits(); - - // change long to double if the sql type is decimal - if(( sql_type == SQL_DECIMAL || sql_type == SQL_NUMERIC ) && Z_TYPE_P(param_z) == IS_LONG ) - convert_to_double( param_z ); - } - else{ - // if the sql type is unknown, then set the default based on the PHP type passed in - if( sql_type == SQL_UNKNOWN_TYPE ){ - default_sql_type( stmt, param_num, param_z, encoding, sql_type ); + } else if (direction == SQL_PARAM_OUTPUT || direction == SQL_PARAM_INPUT_OUTPUT) { + new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param_inout))) sqlsrv_param_inout(param_num, direction, encoding, sql_type, column_size, decimal_digits, php_out_type); + } else { + SQLSRV_ASSERT(false, "sqlsrv_params_container::insert_param - Invalid parameter direction."); + } + stmt->params_container.insert_param(param_num, new_param); + param_ptr = new_param; + new_param.transferred(); } - // if the size is unknown, then set the default based on the PHP type passed in - if( column_size == SQLSRV_UNKNOWN_SIZE ){ - default_sql_size_and_scale( stmt, static_cast(param_num), param_z, encoding, column_size, decimal_digits ); - } - } - // determine the ODBC C type - c_type = default_c_type(stmt, param_num, param_z, sql_type, encoding); + SQLSRV_ASSERT(param_ptr != NULL, "core_sqlsrv_bind_param: param_ptr is null. Something went wrong."); - // set the buffer based on the PHP parameter type - switch( Z_TYPE_P( param_z )){ - - case IS_NULL: - { - SQLSRV_ASSERT( direction == SQL_PARAM_INPUT, "Invalid output param type. The driver layer should catch this." ); - ind_ptr = SQL_NULL_DATA; - buffer = NULL; - buffer_len = 0; - } - break; - case IS_TRUE: - case IS_FALSE: - case IS_LONG: - { - // if it is boolean, set the lval to 0 or 1 - convert_to_long( param_z ); - buffer = ¶m_z->value; - buffer_len = sizeof( Z_LVAL_P( param_z )); - ind_ptr = buffer_len; - if( direction != SQL_PARAM_INPUT ){ - // save the parameter so that 1) the buffer doesn't go away, and 2) we can set it to NULL if returned - sqlsrv_output_param output_param( param_ref, static_cast( param_num ), zval_was_bool, php_out_type); - save_output_param_for_later( stmt, output_param ); - } - } - break; - case IS_DOUBLE: - { - buffer = ¶m_z->value; - buffer_len = sizeof( Z_DVAL_P( param_z )); - ind_ptr = buffer_len; - if( direction != SQL_PARAM_INPUT ){ - // save the parameter so that 1) the buffer doesn't go away, and 2) we can set it to NULL if returned - sqlsrv_output_param output_param( param_ref, static_cast( param_num ), zval_was_bool, php_out_type); - save_output_param_for_later( stmt, output_param ); - } - } - break; - case IS_STRING: - { - // With AE, the precision of the decimal or numeric inputs have to match exactly as defined in the columns. - // Without AE, the derived default sql types will not be this specific. Thus, if sql_type is SQL_DECIMAL - // or SQL_NUMERIC, the user must have clearly specified it (using the SQLSRV driver) as SQL_DECIMAL or SQL_NUMERIC. - // In either case, the input passed into SQLBindParam requires matching scale (i.e., number of decimal digits). - if (sql_type == SQL_DECIMAL || sql_type == SQL_NUMERIC) { - adjustDecimalPrecision(param_z, decimal_digits); - } - - buffer = Z_STRVAL_P( param_z ); - buffer_len = Z_STRLEN_P( param_z ); - - bool is_numeric = is_a_numeric_type(sql_type); - - // if the encoding is UTF-8, translate from UTF-8 to UTF-16 (the type variables should have already been adjusted) - if( direction == SQL_PARAM_INPUT && encoding == CP_UTF8 && !is_numeric){ - - zval wbuffer_z; - ZVAL_NULL( &wbuffer_z ); - - bool converted = convert_input_param_to_utf16( param_z, &wbuffer_z ); - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, - param_num + 1, get_last_error_message() ){ - throw core::CoreException(); - } - buffer = Z_STRVAL_P( &wbuffer_z ); - buffer_len = Z_STRLEN_P( &wbuffer_z ); - add_index_zval(&(stmt->param_input_strings), param_num, &wbuffer_z); - } - ind_ptr = buffer_len; - if( direction != SQL_PARAM_INPUT ){ - // PHP 5.4 added interned strings, so since we obviously want to change that string here in some fashion, - // we reallocate the string if it's interned - if( ZSTR_IS_INTERNED( Z_STR_P( param_z ))){ - core::sqlsrv_zval_stringl( param_z, static_cast(buffer), buffer_len ); - buffer = Z_STRVAL_P( param_z ); - buffer_len = Z_STRLEN_P( param_z ); - } - - // if it's a UTF-8 input output parameter (signified by the C type being SQL_C_WCHAR) - // or if the PHP type is a binary encoded string with a N(VAR)CHAR/NTEXTSQL type, - // convert it to wchar first - if( direction == SQL_PARAM_INPUT_OUTPUT && - ( c_type == SQL_C_WCHAR || - ( c_type == SQL_C_BINARY && - ( sql_type == SQL_WCHAR || - sql_type == SQL_WVARCHAR || - sql_type == SQL_WLONGVARCHAR )))){ - - bool converted = convert_input_param_to_utf16( param_z, param_z ); - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, - param_num + 1, get_last_error_message() ){ - throw core::CoreException(); - } - buffer = Z_STRVAL_P( param_z ); - buffer_len = Z_STRLEN_P( param_z ); - ind_ptr = buffer_len; - } - - // since this is an output string, assure there is enough space to hold the requested size and - // set all the variables necessary (param_z, buffer, buffer_len, and ind_ptr) - resize_output_buffer_if_necessary( stmt, param_z, param_num, encoding, c_type, sql_type, column_size, decimal_digits, - buffer, buffer_len ); - - // save the parameter to be adjusted and/or converted after the results are processed - // no need to use wide chars for numeric types - SQLSRV_ENCODING enc = (is_numeric) ? SQLSRV_ENCODING_CHAR : encoding; - sqlsrv_output_param output_param(param_ref, enc, param_num, static_cast(buffer_len)); - - output_param.saveMetaData(sql_type, column_size, decimal_digits); - - save_output_param_for_later( stmt, output_param ); - - // For output parameters, if we set the column_size to be same as the buffer_len, - // then if there is a truncation due to the data coming from the server being - // greater than the column_size, we don't get any truncation error. In order to - // avoid this silent truncation, we set the column_size to be "MAX" size for - // string types. This will guarantee that there is no silent truncation for - // output parameters. - // if column encryption is enabled, at this point the correct column size has been set by SQLDescribeParam - if( direction == SQL_PARAM_OUTPUT && !stmt->conn->ce_option.enabled ){ - - switch( sql_type ){ - - case SQL_VARBINARY: - case SQL_VARCHAR: - case SQL_WVARCHAR: - column_size = SQL_SS_LENGTH_UNLIMITED; - break; - - default: - break; - } - } - } - } - break; - case IS_RESOURCE: - { - SQLSRV_ASSERT( direction == SQL_PARAM_INPUT, "Invalid output param type. The driver layer should catch this." ); - sqlsrv_stream stream_encoding( param_z, encoding ); - HashTable* streams_ht = Z_ARRVAL( stmt->param_streams ); - core::sqlsrv_zend_hash_index_update_mem( *stmt, streams_ht, param_num, &stream_encoding, sizeof(stream_encoding) ); - buffer = reinterpret_cast( param_num ); - Z_TRY_ADDREF_P( param_z ); // so that it doesn't go away while we're using it - buffer_len = 0; - ind_ptr = SQL_DATA_AT_EXEC; - } - break; - case IS_OBJECT: - { - SQLSRV_ASSERT( direction == SQL_PARAM_INPUT, "Invalid output param type. The driver layer should catch this." ); - zval function_z; - zval buffer_z; - zval format_z; - zval params[1]; - ZVAL_UNDEF( &function_z ); - ZVAL_UNDEF( &buffer_z ); - ZVAL_UNDEF( &format_z ); - ZVAL_UNDEF( params ); - - bool valid_class_name_found = false; - - zend_class_entry *class_entry = Z_OBJCE_P( param_z ); - - while( class_entry != NULL ){ - SQLSRV_ASSERT( class_entry->name != NULL, "core_sqlsrv_bind_param: class_entry->name is NULL." ); - if( class_entry->name->len == DateTime::DATETIME_CLASS_NAME_LEN && class_entry->name != NULL && - stricmp( class_entry->name->val, DateTime::DATETIME_CLASS_NAME ) == 0 ){ - valid_class_name_found = true; - break; - } - - else{ - - // Check the parent - class_entry = class_entry->parent; - } - } - - CHECK_CUSTOM_ERROR( !valid_class_name_found, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_num + 1 ){ + bool result = param_ptr->prepare_param(param_ref, param_z); + if (!result && direction == SQL_PARAM_INPUT_OUTPUT) { + CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_INPUT_OUTPUT_PARAM_TYPE_MATCH, param_num + 1) { throw core::CoreException(); } - - // if the user specifies the 'date' sql type, giving it the normal format will cause a 'date overflow error' - // meaning there is too much information in the character string. If the user specifies the 'datetimeoffset' - // sql type, it lacks the timezone. - if( sql_type == SQL_SS_TIMESTAMPOFFSET ){ - core::sqlsrv_zval_stringl( &format_z, const_cast( DateTime::DATETIMEOFFSET_FORMAT ), - DateTime::DATETIMEOFFSET_FORMAT_LEN ); - } - else if( sql_type == SQL_TYPE_DATE ){ - core::sqlsrv_zval_stringl( &format_z, const_cast( DateTime::DATE_FORMAT ), DateTime::DATE_FORMAT_LEN ); - } - else{ - core::sqlsrv_zval_stringl( &format_z, const_cast( DateTime::DATETIME_FORMAT ), DateTime::DATETIME_FORMAT_LEN ); - } - // call the DateTime::format member function to convert the object to a string that SQL Server understands - core::sqlsrv_zval_stringl( &function_z, "format", sizeof( "format" ) - 1 ); - params[0] = format_z; - // This is equivalent to the PHP code: $param_z->format( $format_z ); where param_z is the - // DateTime object and $format_z is the format string. - int zr = call_user_function( EG( function_table ), param_z, &function_z, &buffer_z, 1, params ); - zend_string_release( Z_STR( format_z )); - zend_string_release( Z_STR( function_z )); - CHECK_CUSTOM_ERROR( zr == FAILURE, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_num + 1 ){ - throw core::CoreException(); - } - buffer = Z_STRVAL( buffer_z ); - zr = add_next_index_zval( &( stmt->param_datetime_buffers ), &buffer_z ); - CHECK_CUSTOM_ERROR( zr == FAILURE, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_num + 1 ){ - throw core::CoreException(); - } - buffer_len = Z_STRLEN( buffer_z ) - 1; - ind_ptr = buffer_len; - break; } - case IS_ARRAY: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_num + 1 ); - break; - default: - DIE( "core_sqlsrv_bind_param: Unsupported PHP type. Only string, float, int, and streams (resource) are supported. " - "It is the responsibilty of the driver layer to convert a parameter to one of these types." ); - break; - } - if( zval_was_null ){ - ind_ptr = SQL_NULL_DATA; - } - - core::SQLBindParameter( stmt, param_num + 1, direction, - c_type, sql_type, column_size, decimal_digits, buffer, buffer_len, &ind_ptr ); - - - // When calling SQLDescribeParam() on a parameter targeting a Datetime column, the return values for ParameterType, ColumnSize and DecimalDigits are SQL_TYPE_TIMESTAMP, 23, and 3 respectively. - // For a parameter targeting a SmallDatetime column, the return values are SQL_TYPE_TIMESTAMP, 16, and 0. Inputting these values into SQLBindParameter() results in Operand type clash error. - // This is because SQL_TYPE_TIMESTAMP corresponds to Datetime2 by default, and conversion of Datetime2 to Datetime and conversion of Datetime2 to SmallDatatime is not allowed with encrypted columns. - // To fix the conversion problem, set the SQL_CA_SS_SERVER_TYPE field of the parameter to SQL_SS_TYPE_DATETIME and SQL_SS_TYPE_SMALLDATETIME respectively for a Datetime and Smalldatetime column. - // Note this must be called after SQLBindParameter() or SQLSetDescField() may fail. - // TODO: how to correctly distinguish datetime from datetime2(3)? Both have the same decimal_digits and column_size - if (stmt->conn->ce_option.enabled && sql_type == SQL_TYPE_TIMESTAMP) { - if (decimal_digits == 3) { - core::SQLSetDescField(stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_DATETIME, SQL_IS_INTEGER); - } else if (decimal_digits == 0 && column_size == 16) { - core::SQLSetDescField(stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_SMALLDATETIME, SQL_IS_INTEGER); + // If Always Encrypted is enabled, transfer the known param meta data if applicable, which might alter param_z for decimal types + if (stmt->conn->ce_option.enabled) { + if (param_ptr->sql_data_type == SQL_UNKNOWN_TYPE || param_ptr->column_size == SQLSRV_UNKNOWN_SIZE) { + // meta data parameters are always sorted based on parameter number + param_ptr->copy_param_meta_ae(param_z, stmt->params_container.params_meta_ae[param_num]); + } + } + + // Get all necessary values to prepare for SQLBindParameter + param_ptr->process_param(stmt, param_z); + param_ptr->bind_param(stmt); + + // When calling SQLDescribeParam() on a parameter targeting a Datetime column, the return values for ParameterType, ColumnSize and DecimalDigits are SQL_TYPE_TIMESTAMP, 23, and 3 respectively. + // For a parameter targeting a SmallDatetime column, the return values are SQL_TYPE_TIMESTAMP, 16, and 0. Inputting these values into SQLBindParameter() results in Operand type clash error. + // This is because SQL_TYPE_TIMESTAMP corresponds to Datetime2 by default, and conversion of Datetime2 to Datetime and conversion of Datetime2 to SmallDatatime is not allowed with encrypted columns. + // To fix the conversion problem, set the SQL_CA_SS_SERVER_TYPE field of the parameter to SQL_SS_TYPE_DATETIME and SQL_SS_TYPE_SMALLDATETIME respectively for a Datetime and Smalldatetime column. + // Note this must be called after SQLBindParameter() or SQLSetDescField() may fail. + // VSO BUG 2693: how to correctly distinguish datetime from datetime2(3)? Both have the same decimal_digits and column_size + if (stmt->conn->ce_option.enabled && param_ptr->sql_data_type == SQL_TYPE_TIMESTAMP) { + if (param_ptr->decimal_digits == 3) { + core::SQLSetDescField(stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_DATETIME, SQL_IS_INTEGER); + } else if (param_ptr->decimal_digits == 0 && param_ptr->column_size == 16) { + core::SQLSetDescField(stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_SMALLDATETIME, SQL_IS_INTEGER); + } } - } } catch( core::CoreException& e ){ stmt->free_param_data(); @@ -771,7 +439,6 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_ } } - // core_sqlsrv_execute // Executes the statement previously prepared // Parameters: @@ -814,8 +481,7 @@ SQLRETURN core_sqlsrv_execute( _Inout_ sqlsrv_stmt* stmt, _In_reads_bytes_(sql_l // if data is needed (streams were bound) and they should be sent at execute time, then do so now if( r == SQL_NEED_DATA && stmt->send_streams_at_exec ) { - - send_param_streams( stmt ); + core_sqlsrv_send_stream_packet(stmt, true); } stmt->new_result_set(); @@ -823,23 +489,16 @@ SQLRETURN core_sqlsrv_execute( _Inout_ sqlsrv_stmt* stmt, _In_reads_bytes_(sql_l // if all the data has been sent and no data was returned then finalize the output parameters if( stmt->send_streams_at_exec && ( r == SQL_NO_DATA || !core_sqlsrv_has_any_result( stmt ))) { + stmt->params_container.finalize_output_parameters(); + } - finalize_output_parameters( stmt ); - } - // stream parameters are sent, clean the Hashtable - if ( stmt->send_streams_at_exec ) { - zend_hash_clean( Z_ARRVAL( stmt->param_streams )); - } return r; } catch( core::CoreException& e ) { // if the statement executed but failed in a subsequent operation before returning, - // we need to cancel the statement and deref the output and stream parameters - if ( stmt->send_streams_at_exec ) { - finalize_output_parameters( stmt, true ); - zend_hash_clean( Z_ARRVAL( stmt->param_streams )); - } + // we need to remove all the parameters and cancel the statement + stmt->params_container.clean_up_param_data(); if( stmt->executed ) { SQLCancel( stmt->handle() ); // stmt->executed = false; should this be reset if something fails? @@ -867,6 +526,10 @@ bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orient "core_sqlsrv_fetch: Invalid value provided for fetch_orientation parameter." ); try { + // first check if the end of all results has been reached + CHECK_CUSTOM_ERROR(stmt->past_next_result_end, stmt, SQLSRV_ERROR_NEXT_RESULT_PAST_END) { + throw core::CoreException(); + } // clear the field cache of the previous fetch zend_hash_clean( Z_ARRVAL( stmt->field_cache )); @@ -1307,7 +970,7 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp if( finalize_output_params ) { // if we're finished processing result sets, handle the output parameters - finalize_output_parameters( stmt ); + stmt->params_container.finalize_output_parameters(); } // mark we are past the end of all results @@ -1324,34 +987,6 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt, _In_ bool finalize_outp } } - -// core_sqlsrv_post_param -// Performs any actions post execution for each parameter. For now it cleans up input parameters memory from the statement -// Parameters: -// stmt - the sqlsrv_stmt structure -// param_num - 0 based index of the parameter -// param_z - parameter value itself. -// Returns: -// Nothing, exception thrown if problem occurs - -void core_sqlsrv_post_param( _Inout_ sqlsrv_stmt* stmt, _In_ zend_ulong param_num, zval* param_z ) -{ - SQLSRV_ASSERT( Z_TYPE( stmt->param_input_strings ) == IS_ARRAY, "Statement input parameter UTF-16 buffers array invalid." ); - SQLSRV_ASSERT( Z_TYPE( stmt->param_streams ) == IS_ARRAY, "Statement input parameter streams array invalid." ); - - // if the parameter was an input string, delete it from the array holding input parameter strings - if( zend_hash_index_exists( Z_ARRVAL( stmt->param_input_strings ), param_num )) { - core::sqlsrv_zend_hash_index_del( *stmt, Z_ARRVAL( stmt->param_input_strings ), param_num ); - } - - // if the parameter was an input stream, decrement our reference to it and delete it from the array holding input streams - // PDO doesn't need the reference count, but sqlsrv does since the stream can be live after sqlsrv_execute by sending it - // with sqlsrv_send_stream_data. - if( zend_hash_index_exists( Z_ARRVAL( stmt->param_streams ), param_num )) { - core::sqlsrv_zend_hash_index_del( *stmt, Z_ARRVAL( stmt->param_streams ), param_num ); - } -} - //Calls SQLSetStmtAttr to set a cursor. void core_sqlsrv_set_scrollable( _Inout_ sqlsrv_stmt* stmt, _In_ unsigned long cursor_type ) { @@ -1461,136 +1096,42 @@ void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_ } } -void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z ) -{ - // zend_is_true does not fail. It either returns true or false. - stmt->send_streams_at_exec = ( zend_is_true( value_z )) ? true : false; -} - - // core_sqlsrv_send_stream_packet // send a single packet from a stream parameter to the database using // ODBC. This will also handle the transition between parameters. It // returns true if it is not done sending, false if it is finished. // return_value is what should be returned to the script if it is -// given. Any errors that occur are posted here. +// given. Any errors that occur will be thrown. // Parameters: // stmt - query to send the next packet for +// get_all - send stream data all at once (false by default) // Returns: // true if more data remains to be sent, false if all data processed -bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt ) +bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt, _In_opt_ bool get_all /*= false*/) { - // if there no current parameter to process, get the next one - // (probably because this is the first call to sqlsrv_send_stream_data) - if( stmt->current_stream.stream_z == NULL ) { - - if( check_for_next_stream_parameter( stmt ) == false ) { - - stmt->current_stream = sqlsrv_stream( NULL, SQLSRV_ENCODING_CHAR ); - stmt->current_stream_read = 0; - return false; - } - } + bool bMore = false; try { - - // get the stream from the zval we bound - php_stream* param_stream = NULL; - core::sqlsrv_php_stream_from_zval_no_verify( *stmt, param_stream, stmt->current_stream.stream_z ); - - // if we're at the end, then reset both current_stream and current_stream_read - if (php_stream_eof(param_stream)) { - // yet return to the very beginning of param_stream since SQLParamData() may ask for the same data again - int ret = php_stream_seek(param_stream, 0, SEEK_SET); - if (ret != 0) { - LOG(SEV_ERROR, "PHP stream: stream seek failed."); - throw core::CoreException(); - } - stmt->current_stream = sqlsrv_stream(NULL, SQLSRV_ENCODING_CHAR); - stmt->current_stream_read = 0; - } - // read the data from the stream, send it via SQLPutData and track how much we've sent. - else { - char buffer[PHP_STREAM_BUFFER_SIZE + 1] = {'\0'}; - std::size_t buffer_size = sizeof( buffer ) - 3; // -3 to preserve enough space for a cut off UTF-8 character - std::size_t read = php_stream_read( param_stream, buffer, buffer_size ); - - if (read > UINT_MAX) - { - LOG(SEV_ERROR, "PHP stream: buffer length exceeded."); - throw core::CoreException(); + if (get_all) { + // send stream data all at once (so no more after this) + stmt->params_container.send_all_packets(stmt); + } else { + bMore = stmt->params_container.send_next_packet(stmt); } - stmt->current_stream_read += static_cast( read ); - if (read == 0) { - // send an empty string, which is what a 0 length does. - char buff[1]; // temp storage to hand to SQLPutData - core::SQLPutData(stmt, buff, 0); + if (!bMore) { + // All resources parameters are sent, so it's time to clean up + stmt->params_container.clean_up_param_data(true); } - else if (read > 0) { - // if this is a UTF-8 stream, then we will use the UTF-8 encoding to determine if we're in the middle of a character - // then read in the appropriate number more bytes and then retest the string. This way we try at most to convert it - // twice. - // If we support other encondings in the future, we'll simply need to read a single byte and then retry the conversion - // since all other MBCS supported by SQL Server are 2 byte maximum size. - if( stmt->current_stream.encoding == CP_UTF8 ) { - - // the size of wbuffer is set for the worst case of UTF-8 to UTF-16 conversion, which is a - // expansion of 2x the UTF-8 size. - SQLWCHAR wbuffer[PHP_STREAM_BUFFER_SIZE + 1] = {L'\0'}; - int wbuffer_size = static_cast( sizeof( wbuffer ) / sizeof( SQLWCHAR )); - DWORD last_error_code = ERROR_SUCCESS; - // buffer_size is the # of wchars. Since it set to stmt->param_buffer_size / 2, this is accurate -#ifndef _WIN32 - int wsize = SystemLocale::ToUtf16Strict( stmt->current_stream.encoding, buffer, static_cast(read), wbuffer, wbuffer_size, &last_error_code ); -#else - int wsize = MultiByteToWideChar( stmt->current_stream.encoding, MB_ERR_INVALID_CHARS, buffer, static_cast( read ), wbuffer, wbuffer_size ); - last_error_code = GetLastError(); -#endif // !_WIN32 - - if( wsize == 0 && last_error_code == ERROR_NO_UNICODE_TRANSLATION ) { - - // this will calculate how many bytes were cut off from the last UTF-8 character and read that many more - // in, then reattempt the conversion. If it fails the second time, then an error is returned. - size_t need_to_read = calc_utf8_missing( stmt, buffer, read ); - // read the missing bytes - size_t new_read = php_stream_read( param_stream, static_cast( buffer ) + read, - need_to_read ); - // if the bytes couldn't be read, then we return an error - CHECK_CUSTOM_ERROR( new_read != need_to_read, stmt, SQLSRV_ERROR_INPUT_STREAM_ENCODING_TRANSLATE, get_last_error_message( ERROR_NO_UNICODE_TRANSLATION )) { - throw core::CoreException(); - } - // try the conversion again with the complete character -#ifndef _WIN32 - wsize = SystemLocale::ToUtf16Strict( stmt->current_stream.encoding, buffer, static_cast(read + new_read), wbuffer, static_cast(sizeof( wbuffer ) / sizeof( SQLWCHAR ))); -#else - wsize = MultiByteToWideChar( stmt->current_stream.encoding, MB_ERR_INVALID_CHARS, buffer, static_cast( read + new_read ), wbuffer, static_cast( sizeof( wbuffer ) / sizeof( wchar_t ))); -#endif //!_WIN32 - // something else must be wrong if it failed - CHECK_CUSTOM_ERROR( wsize == 0, stmt, SQLSRV_ERROR_INPUT_STREAM_ENCODING_TRANSLATE, get_last_error_message( ERROR_NO_UNICODE_TRANSLATION )) { - throw core::CoreException(); - } - } - core::SQLPutData( stmt, wbuffer, wsize * sizeof( SQLWCHAR ) ); - } - else { - core::SQLPutData( stmt, buffer, read ); - } - } - } - - } - catch( core::CoreException& e ) { + } catch (core::CoreException& e) { stmt->free_param_data(); - SQLFreeStmt( stmt->handle(), SQL_RESET_PARAMS ); - SQLCancel( stmt->handle() ); - stmt->current_stream = sqlsrv_stream( NULL, SQLSRV_ENCODING_DEFAULT ); - stmt->current_stream_read = 0; + SQLFreeStmt(stmt->handle(), SQL_RESET_PARAMS); + SQLCancel(stmt->handle()); throw e; } - return true; + return bMore; } void stmt_option_functor::operator()( _Inout_ sqlsrv_stmt* /*stmt*/, stmt_option const* /*opt*/, _In_ zval* /*value_z*/ ) @@ -1606,7 +1147,8 @@ void stmt_option_query_timeout:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_opt void stmt_option_send_at_exec:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) { - core_sqlsrv_set_send_at_exec( stmt, value_z ); + // zend_is_true does not fail. It either returns true or false. + stmt->send_streams_at_exec = (zend_is_true(value_z)); } void stmt_option_buffered_query_limit:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z ) @@ -1616,22 +1158,12 @@ void stmt_option_buffered_query_limit:: operator()( _Inout_ sqlsrv_stmt* stmt, s void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z ) { - if (zend_is_true(value_z)) { - stmt->date_as_string = true; - } - else { - stmt->date_as_string = false; - } + stmt->date_as_string = zend_is_true(value_z); } void stmt_option_format_decimals:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z ) { - if (zend_is_true(value_z)) { - stmt->format_decimals = true; - } - else { - stmt->format_decimals = false; - } + stmt->format_decimals = zend_is_true(value_z); } void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z ) @@ -1641,12 +1173,7 @@ void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op void stmt_option_data_classification:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z ) { - if (zend_is_true(value_z)) { - stmt->data_classification = true; - } - else { - stmt->data_classification = false; - } + stmt->data_classification = zend_is_true(value_z); } // internal function to release the active stream. Called by each main API function @@ -1713,6 +1240,28 @@ bool is_a_numeric_type(_In_ SQLSMALLINT sql_type) return false; } +bool is_a_string_type(_In_ SQLSMALLINT sql_type) +{ + switch (sql_type) { + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_SS_VARIANT: + case SQL_SS_UDT: + case SQL_GUID: + case SQL_SS_XML: + case SQL_CHAR: + case SQL_WCHAR: + case SQL_VARCHAR: + case SQL_WVARCHAR: + case SQL_LONGVARCHAR: + case SQL_WLONGVARCHAR: + return true; + } + + return false; +} + void calc_string_size( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLLEN sql_type, _Inout_ SQLLEN& size ) { try { @@ -1770,10 +1319,8 @@ void calc_string_size( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, } } - // calculates how many characters were cut off from the end of a buffer when reading // in UTF-8 encoded text - size_t calc_utf8_missing( _Inout_ sqlsrv_stmt* stmt, _In_reads_(buffer_end) const char* buffer, _In_ size_t buffer_end ) { const char* last_char = buffer + buffer_end - 1; @@ -1807,7 +1354,6 @@ size_t calc_utf8_missing( _Inout_ sqlsrv_stmt* stmt, _In_reads_(buffer_end) cons return need_to_read; } - // Caller is responsible for freeing the memory allocated for the field_value. // The memory allocation has to happen in the core layer because otherwise // the driver layer would have to calculate size of the field_value @@ -1986,323 +1532,6 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i } } - -// check_for_next_stream_parameter -// see if there is another stream to be sent. Returns true and sets the stream as current in the statement structure, otherwise -// returns false -bool check_for_next_stream_parameter( _Inout_ sqlsrv_stmt* stmt ) -{ - zend_ulong stream_index = 0; - SQLRETURN r = SQL_SUCCESS; - sqlsrv_stream* stream_encoding = NULL; - zval* param_z = NULL; - - // get the index into the streams_ht from the parameter data we set in core_sqlsrv_bind_param - r = core::SQLParamData( stmt, reinterpret_cast( &stream_index ) ); - // if no more data, we've exhausted the bound parameters, so return that we're done - if( SQL_SUCCEEDED( r ) || r == SQL_NO_DATA ) { - - // we're all done, so return false - return false; - } - - HashTable* streams_ht = Z_ARRVAL( stmt->param_streams ); - - // pull out the sqlsrv_encoding struct - stream_encoding = reinterpret_cast(zend_hash_index_find_ptr(streams_ht, stream_index)); - SQLSRV_ASSERT(stream_encoding != NULL, "Stream parameter does not exist"); // if the index isn't in the hash, that's a serious error - - param_z = stream_encoding->stream_z; - - // make the next stream current - stmt->current_stream = sqlsrv_stream( param_z, stream_encoding->encoding ); - stmt->current_stream_read = 0; - - // there are more parameters - return true; -} - - -// utility routine to convert an input parameter from UTF-8 to UTF-16 - -bool convert_input_param_to_utf16( _In_ zval* input_param_z, _Inout_ zval* converted_param_z ) -{ - SQLSRV_ASSERT( input_param_z == converted_param_z || Z_TYPE_P( converted_param_z ) == IS_NULL, - "convert_input_param_z called with invalid parameter states" ); - - const char* buffer = Z_STRVAL_P( input_param_z ); - std::size_t buffer_len = Z_STRLEN_P( input_param_z ); - int wchar_size; - - if (buffer_len > INT_MAX) - { - LOG(SEV_ERROR, "Convert input parameter to utf16: buffer length exceeded."); - throw core::CoreException(); - } - - // if the string is empty, then just return that the conversion succeeded as - // MultiByteToWideChar will "fail" on an empty string. - if( buffer_len == 0 ) { - core::sqlsrv_zval_stringl( converted_param_z, "", 0 ); - return true; - } - -#ifndef _WIN32 - // Declare wchar_size to be the largest possible number of UTF-16 characters after - // conversion, to avoid the performance penalty of calling ToUtf16 - wchar_size = buffer_len; -#else - // Calculate the size of the necessary buffer from the length of the string - - // no performance penalty because MultiByteToWidechar is highly optimised - wchar_size = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast( buffer ), static_cast( buffer_len ), NULL, 0 ); -#endif // !_WIN32 - - // if there was a problem determining the size of the string, return false - if( wchar_size == 0 ) { - return false; - } - sqlsrv_malloc_auto_ptr wbuffer; - wbuffer = reinterpret_cast( sqlsrv_malloc( (wchar_size + 1) * sizeof( SQLWCHAR ) )); - // convert the utf-8 string to a wchar string in the new buffer -#ifndef _WIN32 - int rc = SystemLocale::ToUtf16Strict( CP_UTF8, reinterpret_cast( buffer ), static_cast( buffer_len ), wbuffer, wchar_size ); -#else - int rc = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast( buffer ), static_cast( buffer_len ), wbuffer, wchar_size ); -#endif // !_WIN32 - // if there was a problem converting the string, then free the memory and return false - if( rc == 0 ) { - return false; - } - wchar_size = rc; - - // null terminate the string, set the size within the zval, and return success - wbuffer[ wchar_size ] = L'\0'; - core::sqlsrv_zval_stringl( converted_param_z, reinterpret_cast( wbuffer.get() ), wchar_size * sizeof( SQLWCHAR ) ); - sqlsrv_free(wbuffer); - wbuffer.transferred(); - - return true; -} - -// returns the ODBC C type constant that matches the PHP type and encoding given - -SQLSMALLINT default_c_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_ zval const* param_z, _In_ SQLSMALLINT sql_type, _In_ SQLSRV_ENCODING encoding ) -{ - SQLSMALLINT sql_c_type = SQL_UNKNOWN_TYPE; - int php_type = Z_TYPE_P( param_z ); - - switch( php_type ) { - - case IS_NULL: - switch( encoding ) { - // The c type is set to match to the corresponding sql_type. For NULL cases, if the server type - // is a binary type, than the server expects the sql_type to be binary type as well, otherwise - // an error stating "Implicit conversion not allowed.." is thrown by the server. - // For all other server types, setting the sql_type to sql_char works fine. - case SQLSRV_ENCODING_BINARY: - sql_c_type = SQL_C_BINARY; - break; - default: - sql_c_type = SQL_C_CHAR; - break; - } - break; - case IS_TRUE: - case IS_FALSE: - sql_c_type = SQL_C_SLONG; - break; - case IS_LONG: - // When binding any integer, the zend_long value and its length are used as the buffer - // and buffer length. When the buffer is 8 bytes use the corresponding C type for - // 8-byte integers -#ifdef ZEND_ENABLE_ZVAL_LONG64 - sql_c_type = SQL_C_SBIGINT; -#else - sql_c_type = SQL_C_SLONG; -#endif - break; - case IS_DOUBLE: - sql_c_type = SQL_C_DOUBLE; - break; - case IS_STRING: - switch (encoding) { - case SQLSRV_ENCODING_CHAR: - sql_c_type = SQL_C_CHAR; - break; - case SQLSRV_ENCODING_BINARY: - sql_c_type = SQL_C_BINARY; - break; - case CP_UTF8: - sql_c_type = (is_a_numeric_type(sql_type)) ? SQL_C_CHAR : SQL_C_WCHAR; - break; - default: - THROW_CORE_ERROR(stmt, SQLSRV_ERROR_INVALID_PARAMETER_ENCODING, paramno); - break; - } - break; - case IS_RESOURCE: - switch( encoding ) { - case SQLSRV_ENCODING_CHAR: - sql_c_type = SQL_C_CHAR; - break; - case SQLSRV_ENCODING_BINARY: - sql_c_type = SQL_C_BINARY; - break; - case CP_UTF8: - sql_c_type = SQL_C_WCHAR; - break; - default: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_ENCODING, paramno ); - break; - } - break; - // it is assumed that an object is a DateTime since it's the only thing we support. - // verification that it's a real DateTime object occurs in core_sqlsrv_bind_param. - // we convert the DateTime to a string before sending it to the server. - case IS_OBJECT: - sql_c_type = SQL_C_CHAR; - break; - default: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, paramno ); - break; - } - - return sql_c_type; -} - - -// given a zval and encoding, determine the appropriate sql type -void default_sql_type( _Inout_ sqlsrv_stmt* stmt, _In_opt_ SQLULEN paramno, _In_ zval* param_z, _In_ SQLSRV_ENCODING encoding, - _Out_ SQLSMALLINT& sql_type ) -{ - sql_type = SQL_UNKNOWN_TYPE; - int php_type = Z_TYPE_P(param_z); - switch( php_type ) { - - case IS_NULL: - switch( encoding ) { - // Use the encoding to guess whether the sql_type is binary type or char type. For NULL cases, - // if the server type is a binary type, than the server expects the sql_type to be binary type - // as well, otherwise an error stating "Implicit conversion not allowed.." is thrown by the - // server. For all other server types, setting the sql_type to sql_char works fine. - case SQLSRV_ENCODING_BINARY: - sql_type = SQL_BINARY; - break; - default: - sql_type = SQL_CHAR; - break; - } - break; - case IS_TRUE: - case IS_FALSE: - sql_type = SQL_INTEGER; - break; - case IS_LONG: - //ODBC 64-bit long and integer type are 4 byte values. - if ((Z_LVAL_P(param_z) < INT_MIN) || (Z_LVAL_P(param_z) > INT_MAX)) { - sql_type = SQL_BIGINT; - } - else { - sql_type = SQL_INTEGER; - } - break; - case IS_DOUBLE: - sql_type = SQL_FLOAT; - break; - case IS_RESOURCE: - case IS_STRING: - switch( encoding ) { - case SQLSRV_ENCODING_CHAR: - sql_type = SQL_VARCHAR; - break; - case SQLSRV_ENCODING_BINARY: - sql_type = SQL_VARBINARY; - break; - case CP_UTF8: - sql_type = SQL_WVARCHAR; - break; - default: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_ENCODING, paramno ); - break; - } - break; - // it is assumed that an object is a DateTime since it's the only thing we support. - // verification that it's a real DateTime object occurs in the calling function. - // we convert the DateTime to a string before sending it to the server. - case IS_OBJECT: - // if the user is sending this type to SQL Server 2005 or earlier, make it appear - // as a SQLSRV_SQLTYPE_DATETIME, otherwise it should be SQLSRV_SQLTYPE_TIMESTAMPOFFSET - // since these are the date types of the highest precision for their respective server versions - if( stmt->conn->server_version <= SERVER_VERSION_2005 ) { - sql_type = SQL_TYPE_TIMESTAMP; - } - else { - sql_type = SQL_SS_TIMESTAMPOFFSET; - } - break; - default: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, paramno ); - break; - } - -} - - -// given a zval and encoding, determine the appropriate column size, and decimal scale (if appropriate) - -void default_sql_size_and_scale( _Inout_ sqlsrv_stmt* stmt, _In_opt_ unsigned int paramno, _In_ zval* param_z, _In_ SQLSRV_ENCODING encoding, - _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits ) -{ - int php_type = Z_TYPE_P( param_z ); - column_size = 0; - decimal_digits = 0; - - switch( php_type ) { - - case IS_NULL: - column_size = 1; - break; - // size is not necessary for these types, they are inferred by ODBC - case IS_TRUE: - case IS_FALSE: - case IS_LONG: - case IS_DOUBLE: - case IS_RESOURCE: - break; - case IS_STRING: - { - size_t char_size = (encoding == SQLSRV_ENCODING_UTF8 ) ? sizeof( SQLWCHAR ) : sizeof( char ); - SQLULEN byte_len = Z_STRLEN_P(param_z) * char_size; - if( byte_len > SQL_SERVER_MAX_FIELD_SIZE ) { - column_size = SQL_SERVER_MAX_TYPE_SIZE; - } - else { - column_size = SQL_SERVER_MAX_FIELD_SIZE / char_size; - } - break; - } - // it is assumed that an object is a DateTime since it's the only thing we support. - // verification that it's a real DateTime object occurs in the calling function. - // we convert the DateTime to a string before sending it to the server. - case IS_OBJECT: - // if the user is sending this type to SQL Server 2005 or earlier, make it appear - // as a SQLSRV_SQLTYPE_DATETIME, otherwise it should be SQLSRV_SQLTYPE_TIMESTAMPOFFSET - // since these are the date types of the highest precision for their respective server versions - if( stmt->conn->server_version <= SERVER_VERSION_2005 ) { - column_size = SQL_SERVER_2005_DEFAULT_DATETIME_PRECISION; - decimal_digits = SQL_SERVER_2005_DEFAULT_DATETIME_SCALE; - } - else { - column_size = SQL_SERVER_2008_DEFAULT_DATETIME_PRECISION; - decimal_digits = SQL_SERVER_2008_DEFAULT_DATETIME_SCALE; - } - break; - default: - THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, paramno ); - break; - } -} - void col_cache_dtor( _Inout_ zval* data_z ) { col_cache* cache = static_cast( Z_PTR_P( data_z )); @@ -2392,172 +1621,8 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f *field_len = len; } -// To be called after all results are processed. ODBC and SQL Server do not guarantee that all output -// parameters will be present until all results are processed (since output parameters can depend on results -// while being processed). This function updates the lengths of output parameter strings from the ind_ptr -// parameters passed to SQLBindParameter. It also converts output strings from UTF-16 to UTF-8 if necessary. -// For integer or float parameters, it sets those to NULL if a NULL was returned by SQL Server - -void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt, _In_opt_ bool exception_thrown /*= false*/ ) -{ - if (Z_ISUNDEF(stmt->output_params)) - return; - - // If an error occurs or an exception is thrown during an execution, the values of any output - // parameters or columns are undefined. Therefore, do not depend on them having any specific - // values, because the ODBC driver may or may not have modified them. - if (exception_thrown) { - zend_hash_clean(Z_ARRVAL(stmt->output_params)); - return; - } - - HashTable* params_ht = Z_ARRVAL(stmt->output_params); - zend_ulong index = -1; - zend_string* key = NULL; - void* output_param_temp = NULL; - - try { - ZEND_HASH_FOREACH_KEY_PTR(params_ht, index, key, output_param_temp) - { - sqlsrv_output_param* output_param = static_cast(output_param_temp); - zval* value_z = Z_REFVAL_P(output_param->param_z); - switch (Z_TYPE_P(value_z)) { - case IS_STRING: - { - // adjust the length of the string to the value returned by SQLBindParameter in the ind_ptr parameter - char* str = Z_STRVAL_P(value_z); - SQLLEN str_len = stmt->param_ind_ptrs[output_param->param_num]; - if (str_len == 0) { - core::sqlsrv_zval_stringl(value_z, "", 0); - continue; - } - if (str_len == SQL_NULL_DATA) { - zend_string_release(Z_STR_P(value_z)); - ZVAL_NULL(value_z); - continue; - } - - // if there was more to output than buffer size to hold it, then throw a truncation error - int null_size = 0; - switch (output_param->encoding) { - case SQLSRV_ENCODING_UTF8: - null_size = sizeof(SQLWCHAR); // string isn't yet converted to UTF-8, still UTF-16 - break; - case SQLSRV_ENCODING_SYSTEM: - null_size = 1; - break; - case SQLSRV_ENCODING_BINARY: - null_size = 0; - break; - default: - SQLSRV_ASSERT(false, "Invalid encoding in output_param structure."); - break; - } - CHECK_CUSTOM_ERROR(str_len > (output_param->original_buffer_len - null_size), stmt, - SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, output_param->param_num + 1) - { - throw core::CoreException(); - } - - // For ODBC 11+ see https://msdn.microsoft.com/en-us/library/jj219209.aspx - // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains up to - // output_param->original_buffer_len data and is NULL terminated. - // The IF statement can be true when using connection pooling with unixODBC 2.3.4. - if (str_len == SQL_NO_TOTAL) { - str_len = output_param->original_buffer_len - null_size; - } - - if (output_param->encoding == SQLSRV_ENCODING_BINARY) { - // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated - // so we do that here if the length of the returned data is less than the original allocation. The - // original allocation null terminates the buffer already. - if (str_len < output_param->original_buffer_len) { - str[str_len] = '\0'; - } - core::sqlsrv_zval_stringl(value_z, str, str_len); - } - else { - param_meta_data metaData = output_param->getMetaData(); - - if (output_param->encoding != SQLSRV_ENCODING_CHAR) { - char* outString = NULL; - SQLLEN outLen = 0; - bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen); - CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) - { - throw core::CoreException(); - } - - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, outString, &outLen); - } - - core::sqlsrv_zval_stringl(value_z, outString, outLen); - sqlsrv_free(outString); - } - else { - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len); - } - - core::sqlsrv_zval_stringl(value_z, str, str_len); - } - } - } - break; - case IS_LONG: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { - ZVAL_NULL(value_z); - } - else if (output_param->is_bool) { - convert_to_boolean(value_z); - } - else { - ZVAL_LONG(value_z, static_cast(Z_LVAL_P(value_z))); - } - break; - case IS_DOUBLE: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { - ZVAL_NULL(value_z); - } - else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) { - // first check if its value is out of range - double dval = Z_DVAL_P(value_z); - if (dval > INT_MAX || dval < INT_MIN) { - CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) - { - throw core::CoreException(); - } - } - // if the output param is a boolean, still convert to - // a long integer first to take care of rounding - convert_to_long(value_z); - if (output_param->is_bool) { - convert_to_boolean(value_z); - } - } - break; - default: - DIE("Illegal or unknown output parameter type. This should have been caught in core_sqlsrv_bind_parameter."); - break; - } - value_z = NULL; - } ZEND_HASH_FOREACH_END(); - } - catch (core::CoreException&) { - // empty the hash table due to exception caught - zend_hash_clean(Z_ARRVAL(stmt->output_params)); - throw; - } - // empty the hash table since it's been processed - zend_hash_clean(Z_ARRVAL(stmt->output_params)); - return; -} - -void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type, - _Inout_updates_bytes_(*field_len) void*& field_value, _Inout_ SQLLEN* field_len ) +void get_field_as_string(_Inout_ sqlsrv_stmt *stmt, _In_ SQLUSMALLINT field_index, _Inout_ sqlsrv_phptype sqlsrv_php_type, + _Inout_updates_bytes_(*field_len) void *&field_value, _Inout_ SQLLEN *field_len) { SQLRETURN r; SQLSMALLINT c_type; @@ -2566,7 +1631,7 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind SQLLEN field_len_temp = 0; SQLLEN sql_display_size = 0; char* field_value_temp = NULL; - unsigned int intial_field_len = INITIAL_FIELD_STRING_LEN; + unsigned int initial_field_len = INITIAL_FIELD_STRING_LEN; try { @@ -2605,222 +1670,144 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind if (sqlsrv_php_type.typeinfo.encoding == CP_UTF8 && !is_a_numeric_type(sql_field_type)) { c_type = SQL_C_WCHAR; extra = sizeof(SQLWCHAR); + + sql_display_size = (sql_display_size * sizeof(SQLWCHAR)); } } - // If this is a large type, then read the first few bytes to get the actual length from SQLGetData + // If this is a large type, then read the first chunk to get the actual length from SQLGetData // The user may use "SET TEXTSIZE" to specify the size of varchar(max), nvarchar(max), // varbinary(max), text, ntext, and image data returned by a SELECT statement. - // For varchar(max) and nvarchar(max), sql_display_size will be 0, regardless - if (sql_display_size == 0 || sql_display_size == INT_MAX || - sql_display_size == INT_MAX >> 1 || sql_display_size == UINT_MAX - 1 || - (sql_display_size > SQL_SERVER_MAX_FIELD_SIZE && - (sql_field_type == SQL_WLONGVARCHAR || sql_field_type == SQL_LONGVARCHAR || sql_field_type == SQL_LONGVARBINARY))) { + // For varbinary(max), varchar(max) and nvarchar(max), sql_display_size will be 0, regardless + if (sql_display_size == 0 || + (sql_field_type == SQL_WLONGVARCHAR || sql_field_type == SQL_LONGVARCHAR || sql_field_type == SQL_LONGVARBINARY)) { - field_len_temp = intial_field_len; - - SQLLEN initiallen = field_len_temp + extra; - - field_value_temp = static_cast( sqlsrv_malloc( field_len_temp + extra + 1 )); - - r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp, ( field_len_temp + extra ), - &field_len_temp, false /*handle_warning*/ ); - - CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) { - throw core::CoreException(); - } - - if( field_len_temp == SQL_NULL_DATA ) { - field_value = NULL; - sqlsrv_free( field_value_temp ); - return; - } - - if( r == SQL_SUCCESS_WITH_INFO ) { - - SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = {L'\0'}; - SQLSMALLINT len = 0; - - stmt->current_results->get_diag_field( 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len ); - - // with Linux connection pooling may not get a truncated warning back but the actual field_len_temp - // can be greater than the initallen value. -#ifndef _WIN32 - if( is_truncated_warning( state ) || initiallen < field_len_temp) { -#else - if( is_truncated_warning( state ) ) { -#endif // !_WIN32 - - SQLLEN dummy_field_len = 0; - - // for XML (and possibly other conditions) the field length returned is not the real field length, so - // in every pass, we double the allocation size to retrieve all the contents. - if( field_len_temp == SQL_NO_TOTAL ) { - - // reset the field_len_temp - field_len_temp = intial_field_len; - - do { - SQLLEN initial_field_len = field_len_temp; - // Double the size. - field_len_temp *= 2; - - field_value_temp = static_cast( sqlsrv_realloc( field_value_temp, field_len_temp + extra + 1 )); - - field_len_temp -= initial_field_len; - - // Get the rest of the data. - r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp + initial_field_len, - field_len_temp + extra, &dummy_field_len, false /*handle_warning*/ ); - // the last packet will contain the actual amount retrieved, not SQL_NO_TOTAL - // so we calculate the actual length of the string with that. - if ( dummy_field_len != SQL_NO_TOTAL ) - field_len_temp += dummy_field_len; - else - field_len_temp += initial_field_len; - - if( r == SQL_SUCCESS_WITH_INFO ) { - core::SQLGetDiagField( stmt, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len - ); - } - - } while( r == SQL_SUCCESS_WITH_INFO && is_truncated_warning( state )); - } - else { - // the real field length is returned here, thus no need to double the allocation size here, just have to - // allocate field_len_temp (which is the field length retrieved from the first SQLGetData - field_value_temp = static_cast( sqlsrv_realloc( field_value_temp, field_len_temp + extra + 1 )); - - // We have already received intial_field_len size data. - field_len_temp -= intial_field_len; - - // Get the rest of the data. - r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp + intial_field_len, - field_len_temp + extra, &dummy_field_len, true /*handle_warning*/ ); - field_len_temp += intial_field_len; - - if( dummy_field_len == SQL_NULL_DATA ) { - field_value = NULL; - sqlsrv_free( field_value_temp ); - return; - } - - CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) { - throw core::CoreException(); - } - } - - } // if( is_truncation_warning ( state ) ) - else { - CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { - throw core::CoreException(); - } - } - } // if( r == SQL_SUCCESS_WITH_INFO ) - - if (c_type == SQL_C_WCHAR) { - bool converted = convert_string_from_utf16_inplace( static_cast( sqlsrv_php_type.typeinfo.encoding ), - &field_value_temp, field_len_temp ); - - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { - throw core::CoreException(); - } - } - } // if ( sql_display_size == 0 || sql_display_size == LONG_MAX .. ) - - else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE ) { - - // only allow binary retrievals for char and binary types. All others get a string converted - // to the encoding type they asked for. - - // null terminator - if( c_type == SQL_C_CHAR ) { - sql_display_size += sizeof( SQLCHAR ); - } - - // For WCHAR multiply by sizeof(WCHAR) and include the null terminator - else if( c_type == SQL_C_WCHAR ) { - sql_display_size = (sql_display_size * sizeof(WCHAR)) + sizeof(WCHAR); - } - - field_value_temp = static_cast( sqlsrv_malloc( sql_display_size + extra + 1 )); + field_len_temp = initial_field_len; + field_value_temp = static_cast(sqlsrv_malloc(field_len_temp + extra + 1)); + r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp, (field_len_temp + extra), &field_len_temp, false /*handle_warning*/); + } else { + field_len_temp = sql_display_size; + field_value_temp = static_cast(sqlsrv_malloc(sql_display_size + extra + 1)); // get the data - r = stmt->current_results->get_data( field_index + 1, c_type, field_value_temp, sql_display_size, - &field_len_temp, true /*handle_warning*/ ); - CHECK_SQL_ERROR( r, stmt ) { - throw core::CoreException(); - } - CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) { - throw core::CoreException(); - } - - if( field_len_temp == SQL_NULL_DATA ) { - field_value = NULL; - sqlsrv_free( field_value_temp ); - return; - } - - if (c_type == SQL_C_WCHAR) { - bool converted = convert_string_from_utf16_inplace( static_cast( sqlsrv_php_type.typeinfo.encoding ), - &field_value_temp, field_len_temp ); - - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { - throw core::CoreException(); - } - } - - if (stmt->format_decimals && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) { - // number of decimal places only affect money / smallmoney fields - SQLSMALLINT decimal_places = (stmt->current_meta_data[field_index]->field_is_money_type) ? stmt->decimal_places : NO_CHANGE_DECIMAL_PLACES; - format_decimal_numbers(decimal_places, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp); - } - } // else if( sql_display_size >= 1 && sql_display_size <= SQL_SERVER_MAX_FIELD_SIZE ) - - else { - - DIE( "Invalid sql_display_size" ); - return; // to eliminate a warning + r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp, sql_display_size + extra, &field_len_temp, false /*handle_warning*/); } -field_value = field_value_temp; -*field_len = field_len_temp; + CHECK_CUSTOM_ERROR((r == SQL_NO_DATA), stmt, SQLSRV_ERROR_NO_DATA, field_index) { + throw core::CoreException(); + } + + if (field_len_temp == SQL_NULL_DATA) { + field_value = NULL; + sqlsrv_free(field_value_temp); + return; + } + + if (r == SQL_SUCCESS_WITH_INFO) { + SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = { L'\0' }; + SQLSMALLINT len = 0; + + stmt->current_results->get_diag_field(1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len); + if (is_truncated_warning(state)) { + SQLLEN chunk_field_len = 0; + + // for XML (and possibly other conditions) the field length returned is not the real field length, so + // in every pass, we double the allocation size to retrieve all the contents. + if (field_len_temp == SQL_NO_TOTAL) { + + // reset the field_len_temp + field_len_temp = initial_field_len; + + do { + SQLLEN buffer_len = field_len_temp; + // Double the size. + field_len_temp *= 2; + + field_value_temp = static_cast(sqlsrv_realloc(field_value_temp, field_len_temp + extra + 1)); + + field_len_temp -= buffer_len; + + // Get the rest of the data + r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp + buffer_len, + field_len_temp + extra, &chunk_field_len, false /*handle_warning*/); + // the last packet will contain the actual amount retrieved, not SQL_NO_TOTAL + // so we calculate the actual length of the string with that. + if (chunk_field_len != SQL_NO_TOTAL) + field_len_temp += chunk_field_len; + else + field_len_temp += buffer_len; + + if (r == SQL_SUCCESS_WITH_INFO) { + core::SQLGetDiagField(stmt, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len); + } + } while (r == SQL_SUCCESS_WITH_INFO && is_truncated_warning(state)); + } // if (field_len_temp == SQL_NO_TOTAL) + else { + // The field length (or its estimate) is returned, thus no need to double the allocation size. + // Allocate field_len_temp (which is the field length retrieved from the first SQLGetData) but with some padding + // because there is a chance that the estimated field_len_temp is not accurate enough + SQLLEN buffer_len = 50; + field_value_temp = static_cast(sqlsrv_realloc(field_value_temp, field_len_temp + buffer_len + 1)); + field_len_temp -= initial_field_len; + + // Get the rest of the data + r = stmt->current_results->get_data(field_index + 1, c_type, field_value_temp + initial_field_len, + field_len_temp + buffer_len, &chunk_field_len, false /*handle_warning*/); + field_len_temp = initial_field_len + chunk_field_len; + + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + throw core::CoreException(); + } + + // Reallocate field_value_temp next + field_value_temp = static_cast(sqlsrv_realloc(field_value_temp, field_len_temp + extra + 1)); + } + } // if (is_truncated_warning(state)) + } // if (r == SQL_SUCCESS_WITH_INFO) + + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + throw core::CoreException(); + } + + if (c_type == SQL_C_WCHAR) { + bool converted = convert_string_from_utf16_inplace(static_cast(sqlsrv_php_type.typeinfo.encoding), + &field_value_temp, field_len_temp); + + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + } + + if (stmt->format_decimals && (sql_field_type == SQL_DECIMAL || sql_field_type == SQL_NUMERIC)) { + // number of decimal places only affect money / smallmoney fields + SQLSMALLINT decimal_places = (stmt->current_meta_data[field_index]->field_is_money_type) ? stmt->decimal_places : NO_CHANGE_DECIMAL_PLACES; + format_decimal_numbers(decimal_places, stmt->current_meta_data[field_index]->field_scale, field_value_temp, &field_len_temp); + } + + // finalized the returned values and set field_len to 0 if field_len_temp is negative (which may happen with unixODBC connection pooling) + field_value = field_value_temp; + *field_len = (field_len_temp > 0) ? field_len_temp : 0; // prevent a warning in debug mode about strings not being NULL terminated. Even though nulls are not necessary, the PHP // runtime checks to see if a string is null terminated and issues a warning about it if running in debug mode. - // SQL_C_BINARY fields don't return a NULL terminator, so we allocate an extra byte on each field and use the ternary - // operator to set add 1 to fill the null terminator - - // with unixODBC connection pooling sometimes field_len_temp can be SQL_NO_DATA. - // In that cause do not set null terminator and set length to 0. - if ( field_len_temp > 0 ) - { + // SQL_C_BINARY fields don't return a NULL terminator, so we allocate an extra byte on each field and add 1 to fill the null terminator + if (field_len_temp > 0) { field_value_temp[field_len_temp] = '\0'; } - else - { - *field_len = 0; - } } - - catch( core::CoreException& ) { - + catch (core::CoreException&) { field_value = NULL; *field_len = 0; - sqlsrv_free( field_value_temp ); + sqlsrv_free(field_value_temp); throw; - } - catch ( ... ) { - + } catch (...) { field_value = NULL; *field_len = 0; - sqlsrv_free( field_value_temp ); + sqlsrv_free(field_value_temp); throw; } - } - // return the option from the stmt_opts array that matches the key. If no option found, // NULL is returned. @@ -2869,6 +1856,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) case SQLSRV_PHPTYPE_INT: case SQLSRV_PHPTYPE_FLOAT: case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_TABLE: return true; case SQLSRV_PHPTYPE_STRING: case SQLSRV_PHPTYPE_STREAM: @@ -2884,127 +1872,6 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) return false; } - -// verify there is enough space to hold the output string parameter, and allocate it if needed. The param_z -// is updated to have the new buffer with the correct size and its reference is incremented. The output -// string is place in the stmt->output_params. param_z is modified to hold the new buffer, and buffer, buffer_len and -// stmt->param_ind_ptrs are modified to hold the correct values for SQLBindParameter - -void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z, _In_ SQLULEN paramno, SQLSRV_ENCODING encoding, - _In_ SQLSMALLINT c_type, _In_ SQLSMALLINT sql_type, _In_ SQLULEN column_size, _In_ SQLSMALLINT decimal_digits, - _Out_writes_(buffer_len) SQLPOINTER& buffer, _Out_ SQLLEN& buffer_len ) -{ - SQLSRV_ASSERT( column_size != SQLSRV_UNKNOWN_SIZE, "column size should be set to a known value." ); - buffer_len = Z_STRLEN_P( param_z ); - SQLLEN original_len = buffer_len; - SQLLEN expected_len; - SQLLEN buffer_null_extra; - SQLLEN elem_size; - - // calculate the size of each 'element' represented by column_size. WCHAR is of course 2, - // as is a n(var)char/ntext field being returned as a binary field. - elem_size = (c_type == SQL_C_WCHAR || (c_type == SQL_C_BINARY && (sql_type == SQL_WCHAR || sql_type == SQL_WVARCHAR || sql_type == SQL_WLONGVARCHAR ))) ? 2 : 1; - - // account for the NULL terminator returned by ODBC and needed by Zend to avoid a "String not null terminated" debug warning - SQLULEN field_size = column_size; - // with AE on, when column_size is retrieved from SQLDescribeParam, column_size - // does not include the negative sign or decimal place for numeric values - // VSO Bug 2913: without AE, the same can happen as well, in particular to decimals - // and numerics with precision/scale specified - if (sql_type == SQL_DECIMAL || sql_type == SQL_NUMERIC || sql_type == SQL_BIGINT || sql_type == SQL_INTEGER || sql_type == SQL_SMALLINT) { - // include the possible negative sign - field_size += elem_size; - // include the decimal for output params by adding elem_size - if (decimal_digits > 0) { - field_size += elem_size; - } - } - if (column_size == SQL_SS_LENGTH_UNLIMITED) { - field_size = SQL_SERVER_MAX_FIELD_SIZE / elem_size; - } - expected_len = field_size * elem_size + elem_size; - - // binary fields aren't null terminated, so we need to account for that in our buffer length calcuations - buffer_null_extra = (c_type == SQL_C_BINARY) ? elem_size : 0; - - // increment to include the null terminator since the Zend length doesn't include the null terminator - buffer_len += elem_size; - - // if the current buffer size is smaller than the necessary size, resize the buffer and set the zval to the new - // length. - if( buffer_len < expected_len ) { - SQLSRV_ASSERT( expected_len >= expected_len - buffer_null_extra, - "Integer overflow/underflow caused a corrupt field length." ); - - // allocate enough space to ALWAYS include the NULL regardless of the type being retrieved since - // we set the last byte(s) to be NULL to avoid the debug build warning from the Zend engine about - // not having a NULL terminator on a string. - zend_string* param_z_string = zend_string_realloc( Z_STR_P(param_z), expected_len, 0 ); - - // A zval string len doesn't include the null. This calculates the length it should be - // regardless of whether the ODBC type contains the NULL or not. - - // initialize the newly allocated space - char *p = ZSTR_VAL(param_z_string); - p = p + original_len; - memset(p, '\0', expected_len - original_len); - ZVAL_NEW_STR(param_z, param_z_string); - - // buffer_len is the length passed to SQLBindParameter. It must contain the space for NULL in the - // buffer when retrieving anything but SQLSRV_ENC_BINARY/SQL_C_BINARY - buffer_len = Z_STRLEN_P(param_z) - buffer_null_extra; - - // Zend string length doesn't include the null terminator - ZSTR_LEN(Z_STR_P(param_z)) -= elem_size; - } - - buffer = Z_STRVAL_P(param_z); - - // The StrLen_Ind_Ptr parameter of SQLBindParameter should contain the length of the data to send, which - // may be less than the size of the buffer since the output may be more than the input. If it is greater, - // than the error 22001 is returned by ODBC. - if( stmt->param_ind_ptrs[paramno] > buffer_len - (elem_size - buffer_null_extra)) { - stmt->param_ind_ptrs[paramno] = buffer_len - (elem_size - buffer_null_extra); - } -} - -// output parameters have their reference count incremented so that they do not disappear -// while the query is executed and processed. They are saved in the statement so that -// their reference count may be decremented later (after results are processed) - -void save_output_param_for_later( _Inout_ sqlsrv_stmt* stmt, _Inout_ sqlsrv_output_param& param ) -{ - HashTable* param_ht = Z_ARRVAL( stmt->output_params ); - zend_ulong paramno = static_cast( param.param_num ); - core::sqlsrv_zend_hash_index_update_mem(*stmt, param_ht, paramno, ¶m, sizeof( sqlsrv_output_param )); - Z_TRY_ADDREF_P( param.param_z ); // we have a reference to the param -} - - -// send all the stream data - -void send_param_streams( _Inout_ sqlsrv_stmt* stmt ) -{ - while( core_sqlsrv_send_stream_packet( stmt )) { } -} - - -// called by Zend for each parameter in the sqlsrv_stmt::output_params hash table when it is cleaned/destroyed -void sqlsrv_output_param_dtor( _Inout_ zval* data ) -{ - sqlsrv_output_param *output_param = static_cast( Z_PTR_P( data )); - zval_ptr_dtor( output_param->param_z ); // undo the reference to the string we will no longer hold - sqlsrv_free( output_param ); -} - -// called by Zend for each stream in the sqlsrv_stmt::param_streams hash table when it is cleaned/destroyed -void sqlsrv_stream_dtor( _Inout_ zval* data ) -{ - sqlsrv_stream* stream_encoding = static_cast( Z_PTR_P( data )); - zval_ptr_dtor( stream_encoding->stream_z ); // undo the reference to the stream we will no longer hold - sqlsrv_free( stream_encoding ); -} - void adjustDecimalPrecision(_Inout_ zval* param_z, _In_ SQLSMALLINT decimal_digits) { char* value = Z_STRVAL_P(param_z); @@ -3198,6 +2065,1576 @@ int round_up_decimal_numbers(_Inout_ char* buffer, _In_ int decimal_pos, _In_ in // Do nothing and just return return lastpos; } +} // end of anonymous namespace +//////////////////////////////////////////////////////////////////////////////////////////////// +// +// *** implementations of structures used for SQLBindParameter *** +// +void sqlsrv_param::release_data() +{ + if (Z_TYPE(placeholder_z) == IS_STRING) { + zend_string_release(Z_STR(placeholder_z)); + } + ZVAL_UNDEF(&placeholder_z); + + buffer = NULL; + param_stream = NULL; + num_bytes_read = 0; + param_ptr_z = NULL; +} + +void sqlsrv_param::copy_param_meta_ae(_Inout_ zval* param_z, _In_ param_meta_data& meta) +{ + // Always Encrypted (AE) enabled - copy the meta data from SQLDescribeParam() + sql_data_type = meta.sql_type; + column_size = meta.column_size; + decimal_digits = meta.decimal_digits; + + // Due to strict rules of AE, convert long to double if the sql type is decimal (numeric) + if (Z_TYPE_P(param_z) == IS_LONG && (sql_data_type == SQL_DECIMAL || sql_data_type == SQL_NUMERIC)) { + convert_to_double(param_z); + } +} + +bool sqlsrv_param::prepare_param(_In_ zval* param_ref, _Inout_ zval* param_z) +{ + // For input parameters, check if the original parameter was null + was_null = (Z_TYPE_P(param_z) == IS_NULL); + + return true; +} + +// Derives the ODBC C type constant that matches the PHP type and/or the encoding given +// If SQL type or column size is unknown, derives the appropriate values as well using the provided param zval and encoding +void sqlsrv_param::process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + // Get param php type + param_php_type = Z_TYPE_P(param_z); + + switch (param_php_type) { + case IS_NULL: + process_null_param(param_z); + break; + case IS_TRUE: + case IS_FALSE: + process_bool_param(param_z); + break; + case IS_LONG: + process_long_param(param_z); + break; + case IS_DOUBLE: + process_double_param(param_z); + break; + case IS_STRING: + process_string_param(stmt, param_z); + break; + case IS_RESOURCE: + process_resource_param(param_z); + break; + case IS_OBJECT: + process_object_param(stmt, param_z); + break; + case IS_ARRAY: + default: + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1); + break; + } +} + +void sqlsrv_param::process_null_param(_Inout_ zval* param_z) +{ + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + // Use the encoding to guess whether the sql_type is binary type or char type. For NULL cases, + // if the server type is a binary type, than the server expects the sql_type to be binary type + // as well, otherwise an error stating "Implicit conversion not allowed.." is thrown by the + // server. For all other server types, setting the sql_type to sql_char works fine. + sql_data_type = (encoding == SQLSRV_ENCODING_BINARY) ? SQL_BINARY : SQL_CHAR; + } + + c_data_type = (encoding == SQLSRV_ENCODING_BINARY) ? SQL_C_BINARY : SQL_C_CHAR; + + if (column_size == SQLSRV_UNKNOWN_SIZE) { + column_size = 1; + decimal_digits = 0; + } + buffer = NULL; + buffer_length = 0; + strlen_or_indptr = SQL_NULL_DATA; +} + +void sqlsrv_param::process_bool_param(_Inout_ zval* param_z) +{ + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + sql_data_type = SQL_INTEGER; + } + + c_data_type = SQL_C_SLONG; + + // The column size and decimal digits are by default 0 + // Ignore column_size and decimal_digits because they will be inferred by ODBC + // Convert the lval to 0 or 1 + convert_to_long(param_z); + buffer = ¶m_z->value; + buffer_length = sizeof(Z_LVAL_P(param_z)); + strlen_or_indptr = buffer_length; +} + +void sqlsrv_param::process_long_param(_Inout_ zval* param_z) +{ + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + //ODBC 64-bit long and integer type are 4 byte values. + if ((Z_LVAL_P(param_z) < INT_MIN) || (Z_LVAL_P(param_z) > INT_MAX)) { + sql_data_type = SQL_BIGINT; + } else { + sql_data_type = SQL_INTEGER; + } + } + + // When binding any integer, the zend_long value and its length are used as the buffer + // and buffer length. When the buffer is 8 bytes use the corresponding C type for + // 8-byte integers +#ifdef ZEND_ENABLE_ZVAL_LONG64 + c_data_type = SQL_C_SBIGINT; +#else + c_data_type = SQL_C_SLONG; +#endif + + // The column size and decimal digits are by default 0 + // Ignore column_size and decimal_digits because they will be inferred by ODBC + buffer = ¶m_z->value; + buffer_length = sizeof(Z_LVAL_P(param_z)); + strlen_or_indptr = buffer_length; +} + +void sqlsrv_param::process_double_param(_Inout_ zval* param_z) +{ + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + sql_data_type = SQL_FLOAT; + } + // The column size and decimal digits are by default 0 + // Ignore column_size and decimal_digits because they will be inferred by ODBC + c_data_type = SQL_C_DOUBLE; + + buffer = ¶m_z->value; + buffer_length = sizeof(Z_DVAL_P(param_z)); + strlen_or_indptr = buffer_length; +} + +bool sqlsrv_param::derive_string_types_sizes(_In_ zval* param_z) +{ + SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::derive_string_types_sizes"); + + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + switch (encoding) { + case SQLSRV_ENCODING_CHAR: + sql_data_type = SQL_VARCHAR; + break; + case SQLSRV_ENCODING_BINARY: + sql_data_type = SQL_VARBINARY; + break; + case SQLSRV_ENCODING_UTF8: + sql_data_type = SQL_WVARCHAR; + break; + default: + break; + } + } + + bool is_numeric = is_a_numeric_type(sql_data_type); + + // Derive the C Data type next + switch (encoding) { + case SQLSRV_ENCODING_CHAR: + c_data_type = SQL_C_CHAR; + break; + case SQLSRV_ENCODING_BINARY: + c_data_type = SQL_C_BINARY; + break; + case SQLSRV_ENCODING_UTF8: + c_data_type = is_numeric ? SQL_C_CHAR : SQL_C_WCHAR; + break; + default: + break; + } + + // Derive the column size also only if it is unknown + if (column_size == SQLSRV_UNKNOWN_SIZE) { + size_t char_size = (encoding == SQLSRV_ENCODING_UTF8) ? sizeof(SQLWCHAR) : sizeof(char); + SQLULEN byte_len = Z_STRLEN_P(param_z) * char_size; + + if (byte_len > SQL_SERVER_MAX_FIELD_SIZE) { + column_size = SQL_SERVER_MAX_TYPE_SIZE; + } else { + column_size = SQL_SERVER_MAX_FIELD_SIZE / char_size; + } + } + + return is_numeric; +} + +bool sqlsrv_param::convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +{ + // This converts the string in param_z and stores the wide string in the member placeholder_z + char* str = Z_STRVAL_P(param_z); + SQLLEN str_length = Z_STRLEN_P(param_z); + + if (str_length > 0) { + sqlsrv_malloc_auto_ptr wide_buffer; + unsigned int wchar_size = 0; + + wide_buffer = utf16_string_from_mbcs_string(encoding, reinterpret_cast(str), static_cast(str_length), &wchar_size, true); + if (wide_buffer == 0) { + return false; + } + wide_buffer[wchar_size] = L'\0'; + core::sqlsrv_zval_stringl(&placeholder_z, reinterpret_cast(wide_buffer.get()), wchar_size * sizeof(SQLWCHAR)); + } else { + // If the string is empty, then nothing needs to be done + core::sqlsrv_zval_stringl(&placeholder_z, "", 0); + } + + return true; +} + +void sqlsrv_param::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + bool is_numeric = derive_string_types_sizes(param_z); + + // With AE, the precision of the decimal or numeric inputs have to match exactly as defined in the columns. + // Without AE, the derived default sql types will not be this specific. Thus, if sql_type is SQL_DECIMAL + // or SQL_NUMERIC, the user must have clearly specified it (using the SQLSRV driver) as SQL_DECIMAL or SQL_NUMERIC. + // In either case, the input passed into SQLBindParam requires matching scale (i.e., number of decimal digits). + if (sql_data_type == SQL_DECIMAL || sql_data_type == SQL_NUMERIC) { + adjustDecimalPrecision(param_z, decimal_digits); + } + + if (!is_numeric && encoding == CP_UTF8) { + // Convert the input param value to wide string and save it for later + if (Z_STRLEN_P(param_z) > INT_MAX) { + LOG(SEV_ERROR, "Convert input parameter to utf16: buffer length exceeded."); + throw core::CoreException(); + } + // This changes the member placeholder_z to hold the wide string + bool converted = convert_input_str_to_utf16(stmt, param_z); + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, param_pos + 1, get_last_error_message()) { + throw core::CoreException(); + } + + // Bind the wide string in placeholder_z + buffer = Z_STRVAL(placeholder_z); + buffer_length = Z_STRLEN(placeholder_z); + } else { + buffer = Z_STRVAL_P(param_z); + buffer_length = Z_STRLEN_P(param_z); + } + + strlen_or_indptr = buffer_length; +} + +void sqlsrv_param::process_resource_param(_Inout_ zval* param_z) +{ + SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::get_resource_param_info"); + + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + switch (encoding) { + case SQLSRV_ENCODING_CHAR: + sql_data_type = SQL_VARCHAR; + break; + case SQLSRV_ENCODING_BINARY: + sql_data_type = SQL_VARBINARY; + break; + case SQLSRV_ENCODING_UTF8: + sql_data_type = SQL_WVARCHAR; + break; + default: + break; + } + } + + // The column_size will be inferred by ODBC unless it is SQLSRV_UNKNOWN_SIZE + if (column_size == SQLSRV_UNKNOWN_SIZE) { + column_size = 0; + } + + switch (encoding) { + case SQLSRV_ENCODING_CHAR: + c_data_type = SQL_C_CHAR; + break; + case SQLSRV_ENCODING_BINARY: + c_data_type = SQL_C_BINARY; + break; + case SQLSRV_ENCODING_UTF8: + c_data_type = SQL_C_WCHAR; + break; + default: + break; + } + + param_ptr_z = param_z; + buffer = reinterpret_cast(this); + buffer_length = 0; + strlen_or_indptr = SQL_DATA_AT_EXEC; +} + +bool sqlsrv_param::convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +{ + // This changes the member placeholder_z to hold the converted string of the datetime object + zval function_z; + zval format_z; + zval params[1]; + ZVAL_UNDEF(&function_z); + ZVAL_UNDEF(&format_z); + ZVAL_UNDEF(params); + + // If the user specifies the 'date' sql type, giving it the normal format will cause a 'date overflow error' + // meaning there is too much information in the character string. If the user specifies the 'datetimeoffset' + // sql type, it lacks the timezone. + if (sql_data_type == SQL_SS_TIMESTAMPOFFSET) { + ZVAL_STRINGL(&format_z, DateTime::DATETIMEOFFSET_FORMAT, DateTime::DATETIMEOFFSET_FORMAT_LEN); + } else if (sql_data_type == SQL_TYPE_DATE) { + ZVAL_STRINGL(&format_z, DateTime::DATE_FORMAT, DateTime::DATE_FORMAT_LEN); + } else { + ZVAL_STRINGL(&format_z, DateTime::DATETIME_FORMAT, DateTime::DATETIME_FORMAT_LEN); + } + + // call the DateTime::format member function to convert the object to a string that SQL Server understands + ZVAL_STRINGL(&function_z, "format", sizeof("format") - 1); + //core::sqlsrv_zval_stringl(&function_z, "format", sizeof("format") - 1); + params[0] = format_z; + + // If placeholder_z is a string, release it first before assigning a new string value + if (Z_TYPE(placeholder_z) == IS_STRING && Z_STR(placeholder_z) != NULL) { + zend_string_release(Z_STR(placeholder_z)); + } + + // This is equivalent to the PHP code: $param_z->format($format_z); where param_z is the + // DateTime object and $format_z is the format string. + int zr = call_user_function(EG(function_table), param_z, &function_z, &placeholder_z, 1, params); + + zend_string_release(Z_STR(format_z)); + zend_string_release(Z_STR(function_z)); + + return (zr != FAILURE); +} + +bool sqlsrv_param::preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +{ + bool valid_class_name_found = false; + zend_class_entry *class_entry = Z_OBJCE_P(param_z); + + while (class_entry != NULL) { + SQLSRV_ASSERT(class_entry->name != NULL, "sqlsrv_param::get_object_param_info -- class_entry->name is NULL."); + if (class_entry->name->len == DateTime::DATETIME_CLASS_NAME_LEN && class_entry->name != NULL && + stricmp(class_entry->name->val, DateTime::DATETIME_CLASS_NAME) == 0) { + valid_class_name_found = true; + break; + } else { + // Check the parent + class_entry = class_entry->parent; + } + } + + if (!valid_class_name_found) { + return false; + } + + // Derive the param SQL type only if it is unknown + if (sql_data_type == SQL_UNKNOWN_TYPE) { + // For SQL Server 2005 or earlier, make it a SQLSRV_SQLTYPE_DATETIME. + // Otherwise it should be SQLSRV_SQLTYPE_TIMESTAMPOFFSET because these + // are the date types of the highest precision for the server + if (stmt->conn->server_version <= SERVER_VERSION_2005) { + sql_data_type = SQL_TYPE_TIMESTAMP; + } else { + sql_data_type = SQL_SS_TIMESTAMPOFFSET; + } + } + + c_data_type = SQL_C_CHAR; + + // Derive the column size also only if it is unknown + if (column_size == SQLSRV_UNKNOWN_SIZE) { + if (stmt->conn->server_version <= SERVER_VERSION_2005) { + column_size = SQL_SERVER_2005_DEFAULT_DATETIME_PRECISION; + decimal_digits = SQL_SERVER_2005_DEFAULT_DATETIME_SCALE; + } else { + column_size = SQL_SERVER_2008_DEFAULT_DATETIME_PRECISION; + decimal_digits = SQL_SERVER_2008_DEFAULT_DATETIME_SCALE; + } + } + + return true; +} + +void sqlsrv_param::process_object_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + // Assume the param refers to a DateTime object since it's the only type the drivers support. + // Verification occurs in the calling function as the drivers convert the DateTime object + // to a string before sending it to the server. + bool succeeded = preprocess_datetime_object(stmt, param_z); + if (succeeded) { + succeeded = convert_datetime_to_string(stmt, param_z); + } + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1) { + throw core::CoreException(); + } + + buffer = Z_STRVAL(placeholder_z); + buffer_length = Z_STRLEN(placeholder_z) - 1; + strlen_or_indptr = buffer_length; +} + +void sqlsrv_param::bind_param(_Inout_ sqlsrv_stmt* stmt) +{ + if (was_null) { + strlen_or_indptr = SQL_NULL_DATA; + } + + core::SQLBindParameter(stmt, param_pos + 1, direction, c_data_type, sql_data_type, column_size, decimal_digits, buffer, buffer_length, &strlen_or_indptr); +} + +void sqlsrv_param::init_data_from_zval(_Inout_ sqlsrv_stmt* stmt) +{ + // Get the stream from the param zval value + num_bytes_read = 0; + param_stream = NULL; + core::sqlsrv_php_stream_from_zval_no_verify(*stmt, param_stream, param_ptr_z); +} + +bool sqlsrv_param::send_data_packet(_Inout_ sqlsrv_stmt* stmt) +{ + // Check EOF first + if (php_stream_eof(param_stream)) { + // But return to the very beginning of param_stream since SQLParamData() may ask for the same data again + int ret = php_stream_seek(param_stream, 0, SEEK_SET); + if (ret != 0) { + LOG(SEV_ERROR, "PHP stream: stream seek failed."); + throw core::CoreException(); + } + // Reset num_bytes_read + num_bytes_read = 0; + + return false; + } else { + // Read the data from the stream, send it via SQLPutData and track how much is already sent. + char buffer[PHP_STREAM_BUFFER_SIZE + 1] = { '\0' }; + std::size_t buffer_size = sizeof(buffer) - 3; // -3 to preserve enough space for a cut off UTF-8 character + std::size_t read = php_stream_read(param_stream, buffer, buffer_size); + + if (read > UINT_MAX) { + LOG(SEV_ERROR, "PHP stream: buffer length exceeded."); + throw core::CoreException(); + } + + num_bytes_read += read; + if (read == 0) { + // Send an empty string, which is what a 0 length does. + char buff[1]; // Temp storage to hand to SQLPutData + core::SQLPutData(stmt, buff, 0); + } else if (read > 0) { + // If this is a UTF-8 stream, then we will use the UTF-8 encoding to determine if we're in the middle of a character + // then read in the appropriate number more bytes and then retest the string. This way we try at most to convert it + // twice. + // If we support other encondings in the future, we'll simply need to read a single byte and then retry the conversion + // since all other MBCS supported by SQL Server are 2 byte maximum size. + + if (encoding == CP_UTF8) { + // The size of wbuffer is set for the worst case of UTF-8 to UTF-16 conversion, which is an + // expansion of 2x the UTF-8 size. + SQLWCHAR wbuffer[PHP_STREAM_BUFFER_SIZE + 1] = { L'\0' }; + int wbuffer_size = static_cast(sizeof(wbuffer) / sizeof(SQLWCHAR)); + DWORD last_error_code = ERROR_SUCCESS; + + // The buffer_size is the # of wchars. Set to buffer_size / 2 +#ifndef _WIN32 + int wsize = SystemLocale::ToUtf16Strict(encoding, buffer, static_cast(read), wbuffer, wbuffer_size, &last_error_code); +#else + int wsize = MultiByteToWideChar(encoding, MB_ERR_INVALID_CHARS, buffer, static_cast(read), wbuffer, wbuffer_size); + last_error_code = GetLastError(); +#endif // !_WIN32 + + if (wsize == 0 && last_error_code == ERROR_NO_UNICODE_TRANSLATION) { + // This will calculate how many bytes were cut off from the last UTF-8 character and read that many more + // in, then reattempt the conversion. If it fails the second time, then an error is returned. + size_t need_to_read = calc_utf8_missing(stmt, buffer, read); + // read the missing bytes + size_t new_read = php_stream_read(param_stream, static_cast(buffer) + read, need_to_read); + // if the bytes couldn't be read, then we return an error + CHECK_CUSTOM_ERROR(new_read != need_to_read, stmt, SQLSRV_ERROR_INPUT_STREAM_ENCODING_TRANSLATE, get_last_error_message(ERROR_NO_UNICODE_TRANSLATION)) { + throw core::CoreException(); + } + + // Try the conversion again with the complete character +#ifndef _WIN32 + wsize = SystemLocale::ToUtf16Strict(encoding, buffer, static_cast(read + new_read), wbuffer, static_cast(sizeof(wbuffer) / sizeof(SQLWCHAR))); +#else + wsize = MultiByteToWideChar(encoding, MB_ERR_INVALID_CHARS, buffer, static_cast(read + new_read), wbuffer, static_cast(sizeof(wbuffer) / sizeof(wchar_t))); +#endif //!_WIN32 + // something else must be wrong if it failed + CHECK_CUSTOM_ERROR(wsize == 0, stmt, SQLSRV_ERROR_INPUT_STREAM_ENCODING_TRANSLATE, get_last_error_message(ERROR_NO_UNICODE_TRANSLATION)) { + throw core::CoreException(); + } + } + core::SQLPutData(stmt, wbuffer, wsize * sizeof(SQLWCHAR)); + } + else { + core::SQLPutData(stmt, buffer, read); + } // NOT UTF8 + } // read > 0 + return true; + } // NOT EOF +} + +bool sqlsrv_param_inout::prepare_param(_In_ zval* param_ref, _Inout_ zval* param_z) +{ + // Save the output param reference now + param_ptr_z = param_ref; + + int type = Z_TYPE_P(param_z); + was_null = (type == IS_NULL); + was_bool = (type == IS_TRUE || type == IS_FALSE); + + if (direction == SQL_PARAM_INPUT_OUTPUT) { + // If the user asks for for a specific type for input and output, make sure the data type we send matches the data we + // type we expect back, since we can only send and receive the same type. Anything can be converted to a string, so + // we always let that match if they want a string back. + bool matched = false; + + switch (php_out_type) { + case SQLSRV_PHPTYPE_INT: + if (was_null || was_bool) { + convert_to_long(param_z); + } + matched = (Z_TYPE_P(param_z) == IS_LONG); + break; + case SQLSRV_PHPTYPE_FLOAT: + if (was_null) { + convert_to_double(param_z); + } + matched = (Z_TYPE_P(param_z) == IS_DOUBLE); + break; + case SQLSRV_PHPTYPE_STRING: + // anything can be converted to a string + convert_to_string(param_z); + matched = true; + break; + case SQLSRV_PHPTYPE_NULL: + case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_STREAM: + default: + SQLSRV_ASSERT(false, "sqlsrv_param_inout::prepare_param -- invalid type for an output parameter."); + break; + } + + return matched; + } else if (direction == SQL_PARAM_OUTPUT) { + // If the user specifies a certain type for an output parameter, we have to convert the zval + // to that type so that when the buffer is filled, the type is correct. But first, + // should check if a LOB type is specified. + switch (php_out_type) { + case SQLSRV_PHPTYPE_INT: + convert_to_long(param_z); + break; + case SQLSRV_PHPTYPE_FLOAT: + convert_to_double(param_z); + break; + case SQLSRV_PHPTYPE_STRING: + convert_to_string(param_z); + break; + case SQLSRV_PHPTYPE_NULL: + case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_STREAM: + default: + SQLSRV_ASSERT(false, "sqlsrv_param_inout::prepare_param -- invalid type for an output parameter"); + break; + } + + return true; + } else { + SQLSRV_ASSERT(false, "sqlsrv_param_inout::prepare_param -- wrong param direction."); + } + return false; +} + +// Derives the ODBC C type constant that matches the PHP type and/or the encoding given +// If SQL type or column size is unknown, derives the appropriate values as well using the provided param zval and encoding +void sqlsrv_param_inout::process_param(_Inout_ sqlsrv_stmt* stmt, zval* param_z) +{ + // Get param php type NOW because the original parameter might have been converted beforehand + param_php_type = Z_TYPE_P(param_z); + + switch (param_php_type) { + case IS_LONG: + process_long_param(param_z); + break; + case IS_DOUBLE: + process_double_param(param_z); + break; + case IS_STRING: + process_string_param(stmt, param_z); + break; + default: + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1); + break; + } + + // Save the pointer to the statement object + this->stmt = stmt; +} + +void sqlsrv_param_inout::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + bool is_numeric_type = derive_string_types_sizes(param_z); + + buffer = Z_STRVAL_P(param_z); + buffer_length = Z_STRLEN_P(param_z); + + if (ZSTR_IS_INTERNED(Z_STR_P(param_z))) { + // PHP 5.4 added interned strings, and since we obviously want to change that string here in some fashion, + // we reallocate the string if it's interned + core::sqlsrv_zval_stringl(param_z, static_cast(buffer), buffer_length); + + // reset buffer and its length + buffer = Z_STRVAL_P(param_z); + buffer_length = Z_STRLEN_P(param_z); + } + + // If it's a UTF-8 input output parameter (signified by the C type being SQL_C_WCHAR) + // or if the PHP type is a binary encoded string with a N(VAR)CHAR/NTEXT SQL type, + // convert it to wchar first + if (direction == SQL_PARAM_INPUT_OUTPUT && + (c_data_type == SQL_C_WCHAR || + (c_data_type == SQL_C_BINARY && + (sql_data_type == SQL_WCHAR || sql_data_type == SQL_WVARCHAR || sql_data_type == SQL_WLONGVARCHAR)))) { + + if (buffer_length > 0) { + sqlsrv_malloc_auto_ptr wide_buffer; + unsigned int wchar_size = 0; + + wide_buffer = utf16_string_from_mbcs_string(SQLSRV_ENCODING_UTF8, reinterpret_cast(buffer), static_cast(buffer_length), &wchar_size); + CHECK_CUSTOM_ERROR(wide_buffer == 0, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, param_pos + 1, get_last_error_message()) { + throw core::CoreException(); + } + wide_buffer[wchar_size] = L'\0'; + core::sqlsrv_zval_stringl(param_z, reinterpret_cast(wide_buffer.get()), wchar_size * sizeof(SQLWCHAR)); + buffer = Z_STRVAL_P(param_z); + buffer_length = Z_STRLEN_P(param_z); + } + } + + strlen_or_indptr = buffer_length; + + // Since this is an output string, assure there is enough space to hold the requested size and + // update all the variables accordingly (param_z, buffer, buffer_length, and strlen_or_indptr) + resize_output_string_buffer(param_z, is_numeric_type); + if (is_numeric_type) { + encoding = SQLSRV_ENCODING_CHAR; + } + + // For output parameters, if we set the column_size to be same as the buffer_len, + // then if there is a truncation due to the data coming from the server being + // greater than the column_size, we don't get any truncation error. In order to + // avoid this silent truncation, we set the column_size to be "MAX" size for + // string types. This will guarantee that there is no silent truncation for + // output parameters. + // if column encryption is enabled, at this point the correct column size has been set by SQLDescribeParam + if (direction == SQL_PARAM_OUTPUT && !stmt->conn->ce_option.enabled) { + + switch (sql_data_type) { + case SQL_VARBINARY: + case SQL_VARCHAR: + case SQL_WVARCHAR: + column_size = SQL_SS_LENGTH_UNLIMITED; + break; + + default: + break; + } + } +} + +// Called when the output parameter is ready to be finalized, using the value stored in param_ptr_z +void sqlsrv_param_inout::finalize_output_value() +{ + if (param_ptr_z == NULL) { + return; + } + + zval* value_z = Z_REFVAL_P(param_ptr_z); + + switch (Z_TYPE_P(value_z)) { + case IS_STRING: + finalize_output_string(); + break; + case IS_LONG: + // For a long or a float, simply check if NULL was returned and if so, set the parameter to a PHP null + if (strlen_or_indptr == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } else if (was_bool) { + convert_to_boolean(value_z); + } else { + ZVAL_LONG(value_z, static_cast(Z_LVAL_P(value_z))); + } + break; + case IS_DOUBLE: + // For a long or a float, simply check if NULL was returned and if so, set the parameter to a PHP null + if (strlen_or_indptr == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } else if (php_out_type == SQLSRV_PHPTYPE_INT) { + // First check if its value is out of range + double dval = Z_DVAL_P(value_z); + if (dval > INT_MAX || dval < INT_MIN) { + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) { + throw core::CoreException(); + } + } + // Even if the output param is a boolean, still convert to a long + // integer first to take care of rounding + convert_to_long(value_z); + if (was_bool) { + convert_to_boolean(value_z); + } + } + break; + default: + SQLSRV_ASSERT(false, "Should not have reached here - invalid output parameter type in sqlsrv_param_inout::finalize_output_value."); + break; + } + + value_z = NULL; + param_ptr_z = NULL; // Do not keep the reference now that the output param has been processed +} + +// A helper method called by finalize_output_value() to finalize output string parameters +void sqlsrv_param_inout::finalize_output_string() +{ + zval* value_z = Z_REFVAL_P(param_ptr_z); + + // Adjust the length of the string to the value returned by SQLBindParameter in the strlen_or_indptr argument + if (strlen_or_indptr == 0) { + core::sqlsrv_zval_stringl(value_z, "", 0); + return; + } + if (strlen_or_indptr == SQL_NULL_DATA) { + zend_string_release(Z_STR_P(value_z)); + ZVAL_NULL(value_z); + return; + } + + // If there was more to output than buffer size to hold it, then throw a truncation error + SQLLEN str_len = strlen_or_indptr; + char* str = Z_STRVAL_P(value_z); + int null_size = 0; + + switch (encoding) { + case SQLSRV_ENCODING_UTF8: + null_size = sizeof(SQLWCHAR); // The string isn't yet converted to UTF-8, still UTF-16 + break; + case SQLSRV_ENCODING_SYSTEM: + null_size = sizeof(SQLCHAR); + break; + case SQLSRV_ENCODING_BINARY: + null_size = 0; + break; + default: + SQLSRV_ASSERT(false, "Should not have reached here - invalid encoding in sqlsrv_param_inout::process_output_string."); + break; + } + + CHECK_CUSTOM_ERROR(str_len > (buffer_length - null_size), stmt, SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, param_pos + 1) { + throw core::CoreException(); + } + + // For ODBC 11+ see https://docs.microsoft.com/sql/relational-databases/native-client/features/odbc-driver-behavior-change-when-handling-character-conversions + // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains data up to the + // original buffer_length and is NULL terminated. + // The IF statement can be true when using connection pooling with unixODBC 2.3.4. + if (str_len == SQL_NO_TOTAL) { + str_len = buffer_length - null_size; + } + + if (encoding == SQLSRV_ENCODING_BINARY) { + // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated + // so we do that here if the length of the returned data is less than the original allocation. The + // original allocation null terminates the buffer already. + if (str_len < buffer_length) { + str[str_len] = '\0'; + } + core::sqlsrv_zval_stringl(value_z, str, str_len); + } + else { + if (encoding != SQLSRV_ENCODING_CHAR) { + char* outString = NULL; + SQLLEN outLen = 0; + + bool result = convert_string_from_utf16(encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen); + CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + + if (stmt->format_decimals && (sql_data_type == SQL_DECIMAL || sql_data_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, decimal_digits, outString, &outLen); + } + + core::sqlsrv_zval_stringl(value_z, outString, outLen); + sqlsrv_free(outString); + } + else { + if (stmt->format_decimals && (sql_data_type == SQL_DECIMAL || sql_data_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, decimal_digits, str, &str_len); + } + + core::sqlsrv_zval_stringl(value_z, str, str_len); + } + } + + value_z = NULL; +} + +void sqlsrv_param_inout::resize_output_string_buffer(_Inout_ zval* param_z, _In_ bool is_numeric_type) +{ + // Prerequisites: buffer, buffer_length, column_size, and strlen_or_indptr have been set to a known value + // Purpose: + // Verify there is enough space to hold the output string parameter, and allocate if necessary. The param_z + // is updated to contain the new buffer with the correct size and its reference is incremented, and all required + // values for SQLBindParameter will also be updated. + SQLLEN original_len = buffer_length; + SQLLEN expected_len; + SQLLEN buffer_null_extra; + SQLLEN elem_size; + + // Calculate the size of each 'element' represented by column_size. WCHAR is the size of a wide char (2), and so is + // a N(VAR)CHAR/NTEXT field being returned as a binary field. + elem_size = (c_data_type == SQL_C_WCHAR || + (c_data_type == SQL_C_BINARY && + (sql_data_type == SQL_WCHAR || sql_data_type == SQL_WVARCHAR || sql_data_type == SQL_WLONGVARCHAR))) ? sizeof(SQLWCHAR) : sizeof(SQLCHAR); + + // account for the NULL terminator returned by ODBC and needed by Zend to avoid a "String not null terminated" debug warning + SQLULEN field_size = column_size; + + // With AE enabled, column_size is already retrieved from SQLDescribeParam, but column_size + // does not include the negative sign or decimal place for numeric values + // VSO Bug 2913: without AE, the same can happen as well, in particular to decimals + // and numerics with precision/scale specified + if (is_numeric_type) { + // Include the possible negative sign + field_size += elem_size; + // Include the decimal dot for output params by adding elem_size + if (decimal_digits > 0) { + field_size += elem_size; + } + } + + if (column_size == SQL_SS_LENGTH_UNLIMITED) { + field_size = SQL_SERVER_MAX_FIELD_SIZE / elem_size; + } + expected_len = field_size * elem_size + elem_size; + + // Binary fields aren't null terminated, so we need to account for that in our buffer length calcuations + buffer_null_extra = (c_data_type == SQL_C_BINARY) ? elem_size : 0; + + // Increment to include the null terminator since the Zend length doesn't include the null terminator + buffer_length += elem_size; + + // if the current buffer size is smaller than the necessary size, resize the buffer and set the zval to the new + // length. + if (buffer_length < expected_len) { + SQLSRV_ASSERT(expected_len >= expected_len - buffer_null_extra, "Integer overflow/underflow caused a corrupt field length."); + + // allocate enough space to ALWAYS include the NULL regardless of the type being retrieved since + // we set the last byte(s) to be NULL to avoid the debug build warning from the Zend engine about + // not having a NULL terminator on a string. + zend_string* param_z_string = zend_string_realloc(Z_STR_P(param_z), expected_len, 0); + + // A zval string len doesn't include the null. This calculates the length it should be + // regardless of whether the ODBC type contains the NULL or not. + + // initialize the newly allocated space + char *p = ZSTR_VAL(param_z_string); + p = p + original_len; + memset(p, '\0', expected_len - original_len); + ZVAL_NEW_STR(param_z, param_z_string); + + // buffer_len is the length passed to SQLBindParameter. It must contain the space for NULL in the + // buffer when retrieving anything but SQLSRV_ENC_BINARY/SQL_C_BINARY + buffer_length = Z_STRLEN_P(param_z) - buffer_null_extra; + + // Zend string length doesn't include the null terminator + ZSTR_LEN(Z_STR_P(param_z)) -= elem_size; + } + + buffer = Z_STRVAL_P(param_z); + + // The StrLen_Ind_Ptr parameter of SQLBindParameter should contain the length of the data to send, which + // may be less than the size of the buffer since the output may be more than the input. If it is greater, + // then the error 22001 is returned by ODBC. + if (strlen_or_indptr > buffer_length - (elem_size - buffer_null_extra)) { + strlen_or_indptr = buffer_length - (elem_size - buffer_null_extra); + } +} + +// Change the column encoding based on the sql data type +/*static*/ void sqlsrv_param_tvp::sql_type_to_encoding(_In_ SQLSMALLINT sql_type, _Inout_ SQLSRV_ENCODING* encoding) +{ + switch (sql_type) { + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_BIT: + case SQL_INTEGER: + case SQL_SMALLINT: + case SQL_TINYINT: + case SQL_FLOAT: + case SQL_REAL: + *encoding = SQLSRV_ENCODING_CHAR; + break; + case SQL_BINARY: + case SQL_LONGVARBINARY: + case SQL_VARBINARY: + case SQL_SS_UDT: + *encoding = SQLSRV_ENCODING_BINARY; + break; + default: + // Do nothing + break; + } +} + +void sqlsrv_param_tvp::get_tvp_metadata(_In_ sqlsrv_stmt* stmt, _In_ zend_string* table_type_name, _In_ zend_string* schema_name) +{ + SQLHANDLE chstmt = SQL_NULL_HANDLE; + SQLRETURN rc; + SQLSMALLINT data_type, dec_digits; + SQLINTEGER col_size; + SQLLEN cb_data_type, cb_col_size, cb_dec_digits; + char* table_type = ZSTR_VAL(table_type_name); + + core::SQLAllocHandle(SQL_HANDLE_STMT, *(stmt->conn), &chstmt); + + rc = SQLSetStmtAttr(chstmt, SQL_SOPT_SS_NAME_SCOPE, (SQLPOINTER)SQL_SS_NAME_SCOPE_TABLE_TYPE, SQL_IS_UINTEGER); + CHECK_CUSTOM_ERROR(!SQL_SUCCEEDED(rc), stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + + // Check table type name and see if the schema is specified. Otherwise, assume DBO + if (schema_name != NULL) { + char* schema = ZSTR_VAL(schema_name); + rc = SQLColumns(chstmt, NULL, 0, reinterpret_cast(schema), SQL_NTS, reinterpret_cast(table_type), SQL_NTS, NULL, 0); + } else { + rc = SQLColumns(chstmt, NULL, 0, NULL, SQL_NTS, reinterpret_cast(table_type), SQL_NTS, NULL, 0); + } + + CHECK_CUSTOM_ERROR(!SQL_SUCCEEDED(rc), stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + + SQLSRV_ENCODING stmt_encoding = (stmt->encoding() == SQLSRV_ENCODING_DEFAULT) ? stmt->conn->encoding() : stmt->encoding(); + + if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { + SQLBindCol(chstmt, 5, SQL_C_SSHORT, &data_type, 0, &cb_data_type); + SQLBindCol(chstmt, 7, SQL_C_SLONG, &col_size, 0, &cb_col_size); + SQLBindCol(chstmt, 9, SQL_C_SSHORT, &dec_digits, 0, &cb_dec_digits); + + SQLUSMALLINT pos = 0; + while (SQL_SUCCESS == rc) { + rc = SQLFetch(chstmt); + if (rc == SQL_NO_DATA) { + CHECK_CUSTOM_ERROR(tvp_columns.size() == 0, stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + break; + } + + sqlsrv_malloc_auto_ptr param_ptr; + + // The SQL data type is used to derive the column encoding + SQLSRV_ENCODING column_encoding = stmt_encoding; + sql_type_to_encoding(data_type, &column_encoding); + + param_ptr = new (sqlsrv_malloc(sizeof(sqlsrv_param_tvp))) sqlsrv_param_tvp(pos, column_encoding, data_type, col_size, dec_digits, this); + param_ptr->num_rows = this->num_rows; // Each column inherits the number of rows from the TVP + + tvp_columns[pos] = param_ptr.get(); + param_ptr.transferred(); + + pos++; + } + } else { + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1); + } + + SQLCloseCursor(chstmt); + SQLFreeHandle(SQL_HANDLE_STMT, chstmt); +} + +void sqlsrv_param_tvp::release_data() +{ + // Clean up tvp_columns + std::map::iterator it; + for (it = tvp_columns.begin(); it != tvp_columns.end(); ++it) { + sqlsrv_param_tvp* ptr = it->second; + if (ptr) { + ptr->release_data(); + sqlsrv_free(ptr); + } + } + tvp_columns.clear(); + + sqlsrv_param::release_data(); +} + +void sqlsrv_param_tvp::process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + if (sql_data_type == SQL_SS_TABLE) { + // This is a table-valued parameter + param_php_type = IS_ARRAY; + c_data_type = SQL_C_DEFAULT; + + // The decimal_digits must be 0 for TVP + decimal_digits = 0; + + // The column_size for a TVP is the row array size + // The following method will verify the input array and also derive num_rows + this->num_rows = 0; + int num_columns = parse_tv_param_arrays(stmt, param_z); + column_size = num_rows; + + buffer = NULL; + buffer_length = 0; + strlen_or_indptr = (num_columns == 0)? SQL_DEFAULT_PARAM : SQL_DATA_AT_EXEC; + } else { + // This is one of the constituent columns of the table-valued parameter + // The column value of the first row is already saved in member variable param_ptr_z + process_param_column_value(stmt); + } +} + +int sqlsrv_param_tvp::parse_tv_param_arrays(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + // If this is not a table-valued parameter, simply return + if (sql_data_type != SQL_SS_TABLE) { + return 0; + } + + // This method verifies if the table-valued parameter (i.e. param_z) provided by the user is valid. + // The number of columns in the given table-valued parameter is returned, which may be zero. + HashTable* inputs_ht = Z_ARRVAL_P(param_z); + zend_string *tvp_name = NULL; + zend_string *schema_name = NULL; + zval *tvp_data_z = NULL; + HashPosition pos; + + zend_hash_internal_pointer_reset_ex(inputs_ht, &pos); + if (zend_hash_has_more_elements_ex(inputs_ht, &pos) == SUCCESS) { + + zend_ulong num_index = -1; + size_t key_len = 0; + + int key_type = zend_hash_get_current_key(inputs_ht, &tvp_name, &num_index); + if (key_type == HASH_KEY_IS_STRING) { + key_len = ZSTR_LEN(tvp_name); + tvp_data_z = zend_hash_get_current_data_ex(inputs_ht, &pos); + } + + CHECK_CUSTOM_ERROR((key_type == HASH_KEY_IS_LONG || key_len == 0), stmt, SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, param_pos + 1) { + throw core::CoreException(); + } + } + + // TODO: Find the docs page somewhere that says a TVP can not be null but it may have null columns?? + CHECK_CUSTOM_ERROR(tvp_data_z == NULL || Z_TYPE_P(tvp_data_z) == IS_NULL || Z_TYPE_P(tvp_data_z) != IS_ARRAY, stmt, SQLSRV_ERROR_TVP_INVALID_INPUTS, param_pos + 1) { + throw core::CoreException(); + } + + // Check if schema is provided by the user + if (zend_hash_move_forward_ex(inputs_ht, &pos) == SUCCESS) { + zval *schema_z = zend_hash_get_current_data_ex(inputs_ht, &pos); + if (schema_z != NULL && Z_TYPE_P(schema_z) == IS_STRING) { + schema_name = Z_STR_P(schema_z); + } + } + + // Save the TVP multi-dim array data, which should be something like this + // [ + // [r1c1, r1c2, r1c3], + // [r2c1, r2c2, r2c3], + // [r3c1, r3c2, r3c3] + // ] + param_ptr_z = tvp_data_z; + HashTable* rows_ht = Z_ARRVAL_P(tvp_data_z); + this->num_rows = zend_hash_num_elements(rows_ht); + if (this->num_rows == 0) { + // TVP has no data + return 0; + } + + // Given the table type name, get its column meta data next + size_t total_num_columns = 0; + get_tvp_metadata(stmt, tvp_name, schema_name); + total_num_columns = tvp_columns.size(); + + // (1) Is the array empty? + // (2) Check individual rows and see if their sizes are consistent? + zend_ulong id = -1; + zend_string *key = NULL; + zval* row_z = NULL; + int num_columns = 0; + int type = HASH_KEY_NON_EXISTENT; + + // Loop through the rows to check the number of columns + ZEND_HASH_FOREACH_KEY_VAL(rows_ht, id, key, row_z) { + type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; + CHECK_CUSTOM_ERROR(type == HASH_KEY_IS_STRING, stmt, SQLSRV_ERROR_TVP_STRING_KEYS, param_pos + 1) { + throw core::CoreException(); + } + + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + // Individual row must be an array + CHECK_CUSTOM_ERROR(Z_TYPE_P(row_z) != IS_ARRAY, stmt, SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, param_pos + 1) { + throw core::CoreException(); + } + + // Are all the TVP's rows the same size + num_columns = zend_hash_num_elements(Z_ARRVAL_P(row_z)); + CHECK_CUSTOM_ERROR(num_columns != total_num_columns, stmt, SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, param_pos + 1, total_num_columns) { + throw core::CoreException(); + } + } ZEND_HASH_FOREACH_END(); + + // Return the number of columns + return num_columns; +} + +void sqlsrv_param_tvp::process_param_column_value(_Inout_ sqlsrv_stmt* stmt) +{ + // This is one of the constituent columns of the table-valued parameter + // The corresponding column value of the TVP's first row is already saved in + // the member variable param_ptr_z, which may be a NULL value + zval *data_z = param_ptr_z; + param_php_type = is_a_string_type(sql_data_type) ? IS_STRING : Z_TYPE_P(data_z); + + switch (param_php_type) { + case IS_TRUE: + case IS_FALSE: + case IS_LONG: + case IS_DOUBLE: + sqlsrv_param::process_param(stmt, data_z); + buffer = &placeholder_z.value; // use placeholder zval for binding later + break; + case IS_RESOURCE: + sqlsrv_param::process_resource_param(data_z); + break; + case IS_STRING: + case IS_OBJECT: + if (param_php_type == IS_STRING) { + derive_string_types_sizes(data_z); + } else { + // If preprocessing a datetime object fails, throw an error of invalid php type + bool succeeded = preprocess_datetime_object(stmt, data_z); + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1) { + throw core::CoreException(); + } + } + buffer = reinterpret_cast(this); + buffer_length = 0; + strlen_or_indptr = SQL_DATA_AT_EXEC; + break; + case IS_NULL: + process_null_param_value(stmt); + break; + default: + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1); + break; + } + + // Release the reference + param_ptr_z = NULL; +} + +void sqlsrv_param_tvp::process_null_param_value(_Inout_ sqlsrv_stmt* stmt) +{ + // This is one of the constituent columns of the table-valued parameter + // This method is called when the corresponding column value of the TVP's first row is NULL + // So keep looking in the subsequent rows and find the first non-NULL value in the same column + HashTable* rows_ht = Z_ARRVAL_P(parent_tvp->param_ptr_z); + zval* row_z = NULL; + zval* value_z = NULL; + int php_type = IS_NULL; + int row_id = 1; // Start from the second row + + while ((row_z = zend_hash_index_find(rows_ht, row_id++)) != NULL) { + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + php_type = Z_TYPE_P(value_z); + if (php_type != IS_NULL) { + // Save this non-NULL value before calling process_param_column_value() + param_ptr_z = value_z; + process_param_column_value(stmt); + break; + } + } + + if (php_type == IS_NULL) { + // This means that the entire column contains nothing but NULLs + sqlsrv_param::process_null_param(param_ptr_z); + } +} + +void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt) +{ + core::SQLBindParameter(stmt, param_pos + 1, direction, c_data_type, sql_data_type, column_size, decimal_digits, buffer, buffer_length, &strlen_or_indptr); + + // No need to continue if this is one of the constituent columns of the table-valued parameter + if (sql_data_type != SQL_SS_TABLE) { + return; + } + + if (num_rows == 0) { + // TVP has no data + return; + } + + // Bind the TVP columns one by one + // Register this object first using SQLSetDescField() for sending TVP data post execution + SQLHDESC desc; + core::SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &desc, 0, 0); + SQLRETURN r = ::SQLSetDescField(desc, param_pos + 1, SQL_DESC_DATA_PTR, reinterpret_cast(this), 0); + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + throw core::CoreException(); + } + + // First set focus on this parameter + size_t ordinal = param_pos + 1; + core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast(ordinal), SQL_IS_INTEGER); + + // Bind the TVP columns + HashTable* rows_ht = Z_ARRVAL_P(param_ptr_z); + zval* row_z = zend_hash_index_find(rows_ht, 0); + + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + HashTable* cols_ht = Z_ARRVAL_P(row_z); + zend_ulong id = -1; + zend_string *key = NULL; + zval* data_z = NULL; + int num_columns = 0; + + // In case there are null values in the first row, have to loop + // through the entire first row of column values using the Zend macros. + ZEND_HASH_FOREACH_KEY_VAL(cols_ht, id, key, data_z) { + int type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; + CHECK_CUSTOM_ERROR(type == HASH_KEY_IS_STRING, stmt, SQLSRV_ERROR_TVP_STRING_KEYS, param_pos + 1) { + throw core::CoreException(); + } + + // Assume the user has supplied data for all columns in the right order + SQLUSMALLINT pos = static_cast(id); + sqlsrv_param* column_param = tvp_columns[pos]; + SQLSRV_ASSERT(column_param != NULL, "sqlsrv_param_tvp::bind_param -- column param should not be null"); + + // If data_z is NULL, will need to keep looking in the subsequent rows of + // the same column until a non-null value is found. Since Zend macros must be + // used to traverse the array items, nesting Zend macros in different directions + // does not work. + // Therefore, save data_z for later processing and binding. + column_param->param_ptr_z = data_z; + num_columns++; + } ZEND_HASH_FOREACH_END(); + + // Process the columns and bind each of them using the saved data + for (int i = 0; i < num_columns; i++) { + sqlsrv_param* column_param = tvp_columns[i]; + + column_param->process_param(stmt, NULL); + column_param->bind_param(stmt); + } + + // Reset focus + core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast(0), SQL_IS_INTEGER); +} + +// For each of the constituent columns of the table-valued parameter, check its PHP type +// For pure scalar types, map the cell value (based on current_row and ordinal) to the +// member placeholder_z +void sqlsrv_param_tvp::populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_ int ordinal) +{ + if (sql_data_type == SQL_SS_TABLE || ordinal >= num_rows) { + return; + } + + zval* row_z = NULL; + HashTable* values_ht = NULL; + zval* value_z = NULL; + int type = IS_NULL; + + switch (param_php_type) { + case IS_TRUE: + case IS_FALSE: + case IS_LONG: + case IS_DOUBLE: + // Find the row from the TVP data based on ordinal + row_z = zend_hash_index_find(Z_ARRVAL_P(parent_tvp->param_ptr_z), ordinal); + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + // Now find the column value based on param_pos + value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + type = Z_TYPE_P(value_z); + + // First check if value_z is NULL + if (type == IS_NULL) { + ZVAL_NULL(&placeholder_z); + strlen_or_indptr = SQL_NULL_DATA; + } else { + // Once the placeholder is bound with the correct value from the array, update current_row + if (param_php_type == IS_DOUBLE) { + if (type != IS_DOUBLE) { + // If value_z type is different from param_php_type convert first + convert_to_double(value_z); + } + strlen_or_indptr = sizeof(Z_DVAL_P(value_z)); + ZVAL_DOUBLE(&placeholder_z, Z_DVAL_P(value_z)); + } else { + if (type != IS_LONG) { + // If value_z type is different from param_php_type convert first + // Even for boolean values + convert_to_long(value_z); + } + strlen_or_indptr = sizeof(Z_LVAL_P(value_z)); + ZVAL_LONG(&placeholder_z, Z_LVAL_P(value_z)); + } + } + current_row++; + break; + default: + // Do nothing for non-scalar types + break; + } +} + +// If this is the table-valued parameter, loop through each parameter column +// and populate the cell's placeholder_z. +// If this is one of the constituent columns of the table-valued parameter, +// call SQLPutData() to send the cell value to the server (based on current_row +// and param_pos) +bool sqlsrv_param_tvp::send_data_packet(_Inout_ sqlsrv_stmt* stmt) +{ + if (sql_data_type != SQL_SS_TABLE) { + // This is one of the constituent columns of the table-valued parameter + // Check current_row first + if (current_row >= num_rows) { + return false; + } + + // Find the row from the TVP data based on current_row + zval* row_z = zend_hash_index_find(Z_ARRVAL_P(parent_tvp->param_ptr_z), current_row); + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + // Now find the column value based on param_pos + zval* value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + + // First check if value_z is NULL + if (Z_TYPE_P(value_z) == IS_NULL) { + core::SQLPutData(stmt, NULL, SQL_NULL_DATA); + current_row++; + } else { + switch (param_php_type) { + case IS_RESOURCE: + { + num_bytes_read = 0; + param_stream = NULL; + + // Get the stream from the zval value + core::sqlsrv_php_stream_from_zval_no_verify(*stmt, param_stream, value_z); + // Keep sending the packets until EOF is reached + while (sqlsrv_param::send_data_packet(stmt)) { + } + current_row++; + } + break; + case IS_OBJECT: + { + // This method updates placeholder_z as a string + bool succeeded = convert_datetime_to_string(stmt, value_z); + + // Conversion failed so assume the input was an invalid PHP type + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1) { + throw core::CoreException(); + } + + core::SQLPutData(stmt, Z_STRVAL(placeholder_z), SQL_NTS); + current_row++; + } + break; + case IS_STRING: + { + int type = Z_TYPE_P(value_z); + if (type != IS_STRING) { + convert_to_string(value_z); + } + SQLLEN value_len = Z_STRLEN_P(value_z); + if (value_len == 0) { + // If it's an empty string + core::SQLPutData(stmt, Z_STRVAL_P(value_z), 0); + } else { + if (encoding == CP_UTF8 && !is_a_numeric_type(sql_data_type)) { + if (value_len > INT_MAX) { + LOG(SEV_ERROR, "Convert input parameter to utf16: buffer length exceeded."); + throw core::CoreException(); + } + // This method would change the member placeholder_z + bool succeeded = convert_input_str_to_utf16(stmt, value_z); + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, parent_tvp->param_pos + 1, param_pos + 1, get_last_error_message()) { + throw core::CoreException(); + } + + send_string_data_in_batches(stmt, &placeholder_z); + } else { + send_string_data_in_batches(stmt, value_z); + } + } + current_row++; + } + break; + default: + // Do nothing for basic types as they should be processed elsewhere + break; + } + } // else not IS_NULL + } else { + // This is the table-valued parameter + if (current_row < num_rows) { + // Loop through the table parameter columns and populate each cell's placeholder whenever applicable + for (size_t i = 0; i < tvp_columns.size(); i++) { + tvp_columns[i]->populate_cell_placeholder(stmt, current_row); + } + + // This indicates a TVP row is available + core::SQLPutData(stmt, reinterpret_cast(1), 1); + current_row++; + } else { + // This indicates there is no more TVP row + core::SQLPutData(stmt, reinterpret_cast(0), 0); + } + } + + // Return false to indicate that the current row has been sent + return false; +} + +// A helper method for sending large string data in batches +void sqlsrv_param_tvp::send_string_data_in_batches(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z) +{ + SQLLEN len = Z_STRLEN_P(value_z); + SQLLEN batch = (encoding == CP_UTF8) ? PHP_STREAM_BUFFER_SIZE / sizeof(SQLWCHAR) : PHP_STREAM_BUFFER_SIZE; + + char* p = Z_STRVAL_P(value_z); + while (len > batch) { + core::SQLPutData(stmt, p, batch); + len -= batch; + p += batch; + } + + // Put final batch + core::SQLPutData(stmt, p, len); +} + +void sqlsrv_params_container::clean_up_param_data(_In_opt_ bool only_input/* = false*/) +{ + current_param = NULL; + remove_params(input_params); + if (!only_input) { + remove_params(output_params); + } +} + +// To be called after all results are processed. ODBC and SQL Server do not guarantee that all output +// parameters will be present until all results are processed (since output parameters can depend on results +// while being processed). This function updates the lengths of output parameter strings from the strlen_or_indptr +// argument passed to SQLBindParameter. It also converts output strings from UTF-16 to UTF-8 if necessary. +// If a NULL was returned by SQL Server to any output parameter, set the parameter to NULL as well +void sqlsrv_params_container::finalize_output_parameters() +{ + std::map::iterator it; + for (it = output_params.begin(); it != output_params.end(); ++it) { + sqlsrv_param_inout* ptr = dynamic_cast(it->second); + if (ptr) { + ptr->finalize_output_value(); + } + } +} + +sqlsrv_param* sqlsrv_params_container::find_param(_In_ SQLUSMALLINT param_num, _In_ bool is_input) +{ + try { + if (is_input) { + return input_params.at(param_num); + } else { + return output_params.at(param_num); + } + } catch (std::out_of_range&) { + // not found + return NULL; + } +} + +bool sqlsrv_params_container::get_next_parameter(_Inout_ sqlsrv_stmt* stmt) +{ + // Get the param ptr when binding the resource parameter + SQLPOINTER param = NULL; + SQLRETURN r = core::SQLParamData(stmt, ¶m); + + // If no more data, all the bound parameters have been exhausted, so return false (done) + if (SQL_SUCCEEDED(r) || r == SQL_NO_DATA) { + // Done now, reset current_param + current_param = NULL; + return false; + } + + current_param = reinterpret_cast(param); + SQLSRV_ASSERT(current_param != NULL, "sqlsrv_params_container::get_next_parameter - The parameter requested is missing!"); + current_param->init_data_from_zval(stmt); + + return true; +} + +// The following helper method sends one stream packet at a time, if available +bool sqlsrv_params_container::send_next_packet(_Inout_ sqlsrv_stmt* stmt) +{ + if (current_param == NULL) { + // If current_stream is NULL, either this is the first time checking or the previous parameter + // is done. In either case, MUST call get_next_parameter() to see if there is any more + // parameter requested by ODBC. Otherwise, "Function sequence error" will result, meaning the + // ODBC functions are called out of the order required by the ODBC Specification + if (get_next_parameter(stmt) == false) { + return false; + } + } + + // The helper method send_stream_packet() returns false when EOF is reached + if (current_param->send_data_packet(stmt) == false) { + // Now that EOF has been reached, reset current_param for next round + // Bear in mind that SQLParamData might request the same stream resource again + current_param = NULL; + } + + // Returns true regardless such that either get_next_parameter() will be called or next packet will be sent + return true; } diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index a2288aea..f77e5e59 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -3,7 +3,7 @@ // // Contents: Implementation of PHP streams for reading SQL Server data // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 1b8a2d66..5c79e0b9 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -237,8 +237,8 @@ void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* i ZVAL_UNDEF(params); // Convert the datetime string to a PHP DateTime object - core::sqlsrv_zval_stringl(&value_temp_z, input, length); - core::sqlsrv_zval_stringl(&function_z, "date_create", sizeof("date_create") - 1); + ZVAL_STRINGL(&value_temp_z, input, length); + ZVAL_STRINGL(&function_z, "date_create", sizeof("date_create") - 1); params[0] = value_temp_z; if (call_user_function(EG(function_table), NULL, &function_z, &out_zval, 1, diff --git a/source/shared/globalization.h b/source/shared/globalization.h index 060bbb24..bacfe6bf 100644 --- a/source/shared/globalization.h +++ b/source/shared/globalization.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic.h b/source/shared/interlockedatomic.h index 4e7ef9ca..517a6729 100644 --- a/source/shared/interlockedatomic.h +++ b/source/shared/interlockedatomic.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic_gcc.h b/source/shared/interlockedatomic_gcc.h index fbda0928..582b6710 100644 --- a/source/shared/interlockedatomic_gcc.h +++ b/source/shared/interlockedatomic_gcc.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedslist.h b/source/shared/interlockedslist.h index bb32fb86..ad75f975 100644 --- a/source/shared/interlockedslist.h +++ b/source/shared/interlockedslist.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, singly // linked list. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index da918d6d..a5b1c22f 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -3,7 +3,7 @@ // // Contents: Contains portable classes for localization // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index e0359083..87ed39dc 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -5,7 +5,7 @@ // Must be included in one c/cpp file per binary // A build error will occur if this inclusion policy is not followed // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 436fa7fa..b8ff4019 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -20,7 +20,7 @@ // pecuniary loss) arising out of the use of or inability to use // this SDK, even if Microsoft has been advised of the possibility // of such damages. -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -77,6 +77,8 @@ #define SQL_SOPT_SS_BASE 1225 #define SQL_SOPT_SS_TEXTPTR_LOGGING (SQL_SOPT_SS_BASE+0) // Text pointer logging #define SQL_SOPT_SS_NOBROWSETABLE (SQL_SOPT_SS_BASE+3) // Set NOBROWSETABLE option +#define SQL_SOPT_SS_PARAM_FOCUS (SQL_SOPT_SS_BASE+11)// Direct subsequent calls to parameter related methods to set properties on constituent columns/parameters of container types +#define SQL_SOPT_SS_NAME_SCOPE (SQL_SOPT_SS_BASE+12)// Sets name scope for subsequent catalog function calls #define SQL_SOPT_SS_COLUMN_ENCRYPTION (SQL_SOPT_SS_BASE+13)// Sets the column encryption mode // Define old names #define SQL_TEXTPTR_LOGGING SQL_SOPT_SS_TEXTPTR_LOGGING @@ -180,6 +182,10 @@ #define SQL_COLUMN_ENCRYPTION_DEFAULT SQL_COLUMN_ENCRYPTION_DISABLE // Defines for use with SQL_COPT_SS_CEKCACHETTL #define SQL_CEKCACHETTL_DEFAULT 7200L // TTL value in seconds (2 hours) +//SQL_SOPT_SS_NAME_SCOPE +#define SQL_SS_NAME_SCOPE_TABLE 0L +#define SQL_SS_NAME_SCOPE_TABLE_TYPE 1L +#define SQL_SS_NAME_SCOPE_DEFAULT SQL_SS_NAME_SCOPE_TABLE // SQL_COPT_SS_ENCRYPT #define SQL_EN_OFF 0L #define SQL_EN_ON 1L diff --git a/source/shared/sal_def.h b/source/shared/sal_def.h index a79ee9e3..e87b63e9 100644 --- a/source/shared/sal_def.h +++ b/source/shared/sal_def.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/typedefs_for_linux.h b/source/shared/typedefs_for_linux.h index 8df22852..72806f2c 100644 --- a/source/shared/typedefs_for_linux.h +++ b/source/shared/typedefs_for_linux.h @@ -1,7 +1,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- // File: typedefs_for_linux.h // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/version.h b/source/shared/version.h index 93466519..ebabdfe4 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -4,7 +4,7 @@ // File: version.h // Contents: Version number constants // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -26,12 +26,12 @@ // Increase Minor with backward compatible new functionalities and API changes. // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 -#define SQLVERSION_MINOR 9 +#define SQLVERSION_MINOR 10 #define SQLVERSION_PATCH 0 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1, 2 and so on. Otherwise, set it to 0 -#define PREVIEW 0 +#define PREVIEW 1 #define SEMVER_PRERELEASE // Semantic versioning build metadata, build meta data is not counted in precedence order. @@ -59,7 +59,7 @@ #define _FILEVERSION SQLVERSION_MAJOR,SQLVERSION_MINOR,SQLVERSION_PATCH,SQLVERSION_BUILD // PECL package version ('-' or '+' is not allowed) - to support Pickle do not use macros below -#define PHP_SQLSRV_VERSION "5.9.0" -#define PHP_PDO_SQLSRV_VERSION "5.9.0" +#define PHP_SQLSRV_VERSION "5.10.0beta1" +#define PHP_PDO_SQLSRV_VERSION "5.10.0beta1" #endif // VERSION_H diff --git a/source/shared/xplat.h b/source/shared/xplat.h index 18825555..ebdb20ed 100644 --- a/source/shared/xplat.h +++ b/source/shared/xplat.h @@ -3,7 +3,7 @@ // // Contents: include for definition of Windows types for non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_intsafe.h b/source/shared/xplat_intsafe.h index a1bde6c8..c4d93eeb 100644 --- a/source/shared/xplat_intsafe.h +++ b/source/shared/xplat_intsafe.h @@ -4,7 +4,7 @@ // Contents: This module defines helper functions to prevent // integer overflow bugs. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winerror.h b/source/shared/xplat_winerror.h index 1de5524a..8aad6e23 100644 --- a/source/shared/xplat_winerror.h +++ b/source/shared/xplat_winerror.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winnls.h b/source/shared/xplat_winnls.h index a6698322..6cfc9071 100644 --- a/source/shared/xplat_winnls.h +++ b/source/shared/xplat_winnls.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index 42fb77e8..f06290ea 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.9 for PHP for SQL Server +dnl Microsoft Drivers 5.10 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index bb8d1ecb..68a2d220 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 0c2680cb..c582c774 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use connection handles // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -30,6 +30,8 @@ extern "C" { namespace { +const int MAX_CONN_VALSTRING_LEN = 256; + // current subsytem. defined for the CHECK_SQL_{ERROR|WARNING} macros unsigned int current_log_subsystem = LOG_CONN; @@ -38,13 +40,7 @@ struct date_as_string_func { static void func( connection_option const* /*option*/, _In_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/ ) { ss_sqlsrv_conn* ss_conn = static_cast( conn ); - - if( zend_is_true( value )) { - ss_conn->date_as_string = true; - } - else { - ss_conn->date_as_string = false; - } + ss_conn->date_as_string = zend_is_true(value); } }; @@ -53,13 +49,7 @@ struct format_decimals_func static void func(connection_option const* /*option*/, _In_ zval* value, _Inout_ sqlsrv_conn* conn, std::string& /*conn_str*/) { ss_sqlsrv_conn* ss_conn = static_cast(conn); - - if (zend_is_true(value)) { - ss_conn->format_decimals = true; - } - else { - ss_conn->format_decimals = false; - } + ss_conn->format_decimals = zend_is_true(value); } }; @@ -117,17 +107,10 @@ struct bool_conn_str_func { static void func( _In_ connection_option const* option, _In_ zval* value, sqlsrv_conn* /*conn*/, _Out_ std::string& conn_str ) { - char const* val_str; - if( zend_is_true( value )) { - val_str = "yes"; - } - else { - val_str = "no"; - } - conn_str += option->odbc_name; - conn_str += "={"; - conn_str += val_str; - conn_str += "};"; + char temp_str[MAX_CONN_VALSTRING_LEN]; + + snprintf(temp_str, MAX_CONN_VALSTRING_LEN, "%s={%s};", option->odbc_name, (zend_is_true(value) ? "yes" : "no")); + conn_str += temp_str; } }; @@ -137,12 +120,10 @@ struct int_conn_str_func { { SQLSRV_ASSERT( Z_TYPE_P( value ) == IS_LONG, "An integer is expected for this keyword" ) - std::string val_str = std::to_string( Z_LVAL_P( value )); - - conn_str += option->odbc_name; - conn_str += "={"; - conn_str += val_str; - conn_str += "};"; + char temp_str[MAX_CONN_VALSTRING_LEN]; + + snprintf(temp_str, MAX_CONN_VALSTRING_LEN, "%s={%ld};", option->odbc_name, Z_LVAL_P(value)); + conn_str += temp_str; } }; diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index ab8f282c..a6f6e711 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -2,7 +2,7 @@ // File: init.cpp // Contents: initialization routines for the extension // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -335,6 +335,7 @@ PHP_MINIT_FUNCTION(sqlsrv) REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_INT", SQLSRV_PHPTYPE_INT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_FLOAT", SQLSRV_PHPTYPE_FLOAT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_DATETIME", SQLSRV_PHPTYPE_DATETIME, CONST_PERSISTENT | CONST_CS ); + REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_TABLE", SQLSRV_PHPTYPE_TABLE, CONST_PERSISTENT | CONST_CS); std::string bin = "binary"; std::string chr = "char"; @@ -377,6 +378,7 @@ PHP_MINIT_FUNCTION(sqlsrv) REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TIMESTAMP", constant_type.value, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TINYINT", SQL_TINYINT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_UDT", SQL_SS_UDT, CONST_PERSISTENT | CONST_CS ); + REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TABLE", SQL_SS_TABLE, CONST_PERSISTENT | CONST_CS); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_UNIQUEIDENTIFIER", SQL_GUID, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_XML", SQL_SS_XML, CONST_PERSISTENT | CONST_CS ); constant_type.typeinfo.type = SQL_TYPE_DATE; diff --git a/source/sqlsrv/php_sqlsrv.h b/source/sqlsrv/php_sqlsrv.h index fcfb387a..b30b41c6 100644 --- a/source/sqlsrv/php_sqlsrv.h +++ b/source/sqlsrv/php_sqlsrv.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index e849b758..8937e821 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 2aef9dd6..609209c3 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use statement handles // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -64,7 +64,7 @@ enum SQLSRV_PHPTYPE zend_to_sqlsrv_phptype[] = { SQLSRV_PHPTYPE_INT, SQLSRV_PHPTYPE_FLOAT, SQLSRV_PHPTYPE_STRING, - SQLSRV_PHPTYPE_INVALID, + SQLSRV_PHPTYPE_TABLE, SQLSRV_PHPTYPE_DATETIME, SQLSRV_PHPTYPE_STREAM, SQLSRV_PHPTYPE_INVALID, @@ -96,25 +96,17 @@ sqlsrv_phptype determine_sqlsrv_php_type( sqlsrv_stmt const* stmt, SQLINTEGER sq void determine_stmt_has_rows( _Inout_ ss_sqlsrv_stmt* stmt ); bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ); bool is_valid_sqlsrv_sqltype( _In_ sqlsrv_sqltype type ); -void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, zend_ulong index, _Out_ SQLSMALLINT& direction, - _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, - _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits ); void type_and_encoding( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ); void type_and_size_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ); void type_and_precision_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ); bool verify_and_set_encoding( _In_ const char* encoding_string, _Inout_ sqlsrv_phptype& phptype_encoding ); +zval* parse_param_array(_Inout_ ss_sqlsrv_stmt* stmt, _Inout_ HashTable* param_ht, zend_ulong index, + _Out_ SQLSMALLINT& direction, _Out_ SQLSRV_PHPTYPE& php_out_type, + _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, + _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits); } -// internal helper function to free meta data structures allocated -void meta_data_free( _Inout_ field_meta_data* meta ) -{ - if( meta->field_name ) { - meta->field_name.reset(); - } - sqlsrv_free( meta ); -} - // query options for cursor types namespace SSCursorTypes { @@ -144,9 +136,6 @@ ss_sqlsrv_stmt::ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void ) { - std::for_each(current_meta_data.begin(), current_meta_data.end(), meta_data_free); - current_meta_data.clear(); - if( fetch_field_names != NULL ) { for( int i=0; i < fetch_fields_count; ++i ) { @@ -239,7 +228,9 @@ sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _ case SQL_REAL: ss_phptype.typeinfo.type = SQLSRV_PHPTYPE_FLOAT; break; - + case SQL_SS_TABLE: + ss_phptype.typeinfo.type = SQLSRV_PHPTYPE_TABLE; + break; case SQL_TYPE_DATE: case SQL_SS_TIMESTAMPOFFSET: case SQL_SS_TIME2: @@ -578,8 +569,7 @@ PHP_FUNCTION( sqlsrv_next_result ) core_sqlsrv_next_result( stmt, true ); // clear the current meta data since the new result will generate new meta data - std::for_each(stmt->current_meta_data.begin(), stmt->current_meta_data.end(), meta_data_free); - stmt->current_meta_data.clear(); + stmt->clean_up_results_metadata(); if( stmt->past_next_result_end ) { @@ -1205,50 +1195,70 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt ) HashTable* params_ht = Z_ARRVAL_P( params_z ); - zend_ulong index = -1; - zend_string *key = NULL; - zval* param_z = NULL; + zend_ulong index = -1; + zend_string *key = NULL; + zval* param_z = NULL; - ZEND_HASH_FOREACH_KEY_VAL( params_ht, index, key, param_z ) { - zval* value_z = NULL; - SQLSMALLINT direction = SQL_PARAM_INPUT; - SQLSRV_ENCODING encoding = stmt->encoding(); - if( stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) { - encoding = stmt->conn->encoding(); - } - SQLSMALLINT sql_type = SQL_UNKNOWN_TYPE; - SQLULEN column_size = SQLSRV_UNKNOWN_SIZE; - SQLSMALLINT decimal_digits = 0; - SQLSRV_PHPTYPE php_out_type = SQLSRV_PHPTYPE_INVALID; + ZEND_HASH_FOREACH_KEY_VAL( params_ht, index, key, param_z ) { + // make sure it's an integer index + int type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; + CHECK_CUSTOM_ERROR(type != HASH_KEY_IS_LONG, stmt, SS_SQLSRV_ERROR_PARAM_INVALID_INDEX) { + throw ss::SSException(); + } - // make sure it's an integer index - int type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; - CHECK_CUSTOM_ERROR( type != HASH_KEY_IS_LONG, stmt, SS_SQLSRV_ERROR_PARAM_INVALID_INDEX ) { - throw ss::SSException(); - } + zval* value_z = NULL; + SQLSMALLINT direction = SQL_PARAM_INPUT; + SQLSRV_ENCODING encoding = stmt->encoding(); + if( stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) { + encoding = stmt->conn->encoding(); + } + SQLSMALLINT sql_type = SQL_UNKNOWN_TYPE; + SQLULEN column_size = SQLSRV_UNKNOWN_SIZE; + SQLSMALLINT decimal_digits = 0; + SQLSRV_PHPTYPE php_out_type = SQLSRV_PHPTYPE_INVALID; // if it's a parameter array - if( Z_TYPE_P( param_z ) == IS_ARRAY ) { - - zval* var = NULL; - int zr = ( NULL != ( var = zend_hash_index_find( Z_ARRVAL_P( param_z ), 0 ))) ? SUCCESS : FAILURE; - CHECK_CUSTOM_ERROR( zr == FAILURE, stmt, SS_SQLSRV_ERROR_VAR_REQUIRED, index + 1 ) { - throw ss::SSException(); + if (Z_TYPE_P(param_z) == IS_ARRAY) { + try { + HashTable* param_ht = Z_ARRVAL_P(param_z); + // Check the number of elements in the array + int num_elems = zend_hash_num_elements(param_ht); + if (num_elems > 1) { + value_z = parse_param_array(stmt, param_ht, index, direction, php_out_type, encoding, sql_type, column_size, decimal_digits); + } else { + // Simply get the first variable and use the defaults + value_z = zend_hash_index_find(param_ht, 0); + if (value_z == NULL) { + THROW_SS_ERROR(stmt, SS_SQLSRV_ERROR_VAR_REQUIRED, index + 1); + } + } + } catch (core::CoreException&) { + SQLFreeStmt(stmt->handle(), SQL_RESET_PARAMS); + throw; } - - // parse the parameter array that the user gave - parse_param_array( stmt, param_z, index, direction, php_out_type, encoding, sql_type, column_size, - decimal_digits ); - value_z = var; } else { - CHECK_CUSTOM_ERROR( !stmt->prepared && stmt->conn->ce_option.enabled, stmt, SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED ) { + CHECK_CUSTOM_ERROR(!stmt->prepared && stmt->conn->ce_option.enabled, stmt, SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED) { throw ss::SSException(); } value_z = param_z; } + + // If the user specifies a certain type for an output parameter, we have to convert the zval + // to that type so that when the buffer is filled, the type is correct. But first, + // should check if a LOB type is specified. + CHECK_CUSTOM_ERROR(direction != SQL_PARAM_INPUT && (sql_type == SQL_LONGVARCHAR + || sql_type == SQL_WLONGVARCHAR || sql_type == SQL_LONGVARBINARY), + stmt, SQLSRV_ERROR_OUTPUT_PARAM_TYPES_NOT_SUPPORTED) { + throw core::CoreException(); + } + + // Table-valued parameters are input-only + CHECK_CUSTOM_ERROR(direction != SQL_PARAM_INPUT && (sql_type == SQL_SS_TABLE || php_out_type == SQLSRV_PHPTYPE_TABLE), stmt, SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY) { + throw ss::SSException(); + } + // bind the parameter - SQLSRV_ASSERT( value_z != NULL, "bind_params: value_z is null." ); core_sqlsrv_bind_param( stmt, static_cast( index ), direction, value_z, php_out_type, encoding, sql_type, column_size, decimal_digits ); @@ -1564,6 +1574,7 @@ bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sq *column_size = INT_MAX >> 1; break; case SQL_SS_XML: + case SQL_SS_TABLE: *column_size = SQL_SS_LENGTH_UNLIMITED; break; case SQL_BINARY: @@ -1713,8 +1724,13 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ } break; } + case SQL_SS_TABLE: + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_TABLE; + sqlsrv_phptype.typeinfo.encoding = stmt->encoding(); + break; default: sqlsrv_phptype.typeinfo.type = PHPTYPE_INVALID; + SQLSRV_ASSERT(false, "An invalid php type was returned with (supposedly) validated sql type and column_size"); break; } @@ -1896,179 +1912,106 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ } -void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, zend_ulong index, _Out_ SQLSMALLINT& direction, +zval* parse_param_array(_Inout_ ss_sqlsrv_stmt* stmt, _Inout_ HashTable* param_ht, zend_ulong index, _Out_ SQLSMALLINT& direction, _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, - _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits ) + _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits) { - zval* var_or_val = NULL; - zval* temp = NULL; - HashTable* param_ht = Z_ARRVAL_P( param_array ); - sqlsrv_sqltype sqlsrv_sql_type; - HashPosition pos; + zval* var_or_val = zend_hash_index_find(param_ht, 0); + bool php_type_param_is_null = true; + bool sql_type_param_is_null = true; - try { + // Assumption: there are more than only the variable, parse the rest of the array + zval* dir = zend_hash_index_find(param_ht, 1); + if (Z_TYPE_P(dir) != IS_NULL) { + // if param direction is specified, make sure it's valid + CHECK_CUSTOM_ERROR(Z_TYPE_P(dir) != IS_LONG, stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, index + 1) { + throw ss::SSException(); + } + direction = static_cast(Z_LVAL_P(dir)); - bool php_type_param_was_null = true; - bool sql_type_param_was_null = true; - - php_out_type = SQLSRV_PHPTYPE_INVALID; - encoding = SQLSRV_ENCODING_INVALID; - - // handle the array parameters that contain the value/var, direction, php_type, sql_type - zend_hash_internal_pointer_reset_ex( param_ht, &pos ); - if( zend_hash_has_more_elements_ex( param_ht, &pos ) == FAILURE || - (var_or_val = zend_hash_get_current_data_ex(param_ht, &pos)) == NULL) { - - THROW_SS_ERROR( stmt, SS_SQLSRV_ERROR_VAR_REQUIRED, index + 1 ); + CHECK_CUSTOM_ERROR(direction != SQL_PARAM_INPUT && direction != SQL_PARAM_OUTPUT && direction != SQL_PARAM_INPUT_OUTPUT, + stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, index + 1) { + throw ss::SSException(); + } + CHECK_CUSTOM_ERROR(direction != SQL_PARAM_INPUT && !Z_ISREF_P(var_or_val), stmt, SS_SQLSRV_ERROR_PARAM_VAR_NOT_REF, index + 1) { + throw ss::SSException(); + } } - // if the direction is included, then use what they gave, otherwise INPUT is assumed - if ( zend_hash_move_forward_ex( param_ht, &pos ) == SUCCESS && ( temp = zend_hash_get_current_data_ex( param_ht, &pos )) != NULL && - Z_TYPE_P( temp ) != IS_NULL ) { + // Check if the user provides php type or sql type or both + zval* phptype_z = zend_hash_index_find(param_ht, 2); + zval* sqltype_z = zend_hash_index_find(param_ht, 3); - CHECK_CUSTOM_ERROR( Z_TYPE_P( temp ) != IS_LONG, stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, index + 1 ) { + php_type_param_is_null = (phptype_z == NULL || Z_TYPE_P(phptype_z) == IS_NULL); + sql_type_param_is_null = (sqltype_z == NULL || Z_TYPE_P(sqltype_z) == IS_NULL); - throw ss::SSException(); + if (php_type_param_is_null) { + // so set default for php type based on the variable + if (Z_ISREF_P(var_or_val)) { + php_out_type = zend_to_sqlsrv_phptype[Z_TYPE_P(Z_REFVAL_P(var_or_val))]; + } else { + php_out_type = zend_to_sqlsrv_phptype[Z_TYPE_P(var_or_val)]; } - direction = static_cast( Z_LVAL_P( temp )); - CHECK_CUSTOM_ERROR( direction != SQL_PARAM_INPUT && direction != SQL_PARAM_OUTPUT && direction != SQL_PARAM_INPUT_OUTPUT, - stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, index + 1 ) { + } else { + CHECK_CUSTOM_ERROR(Z_TYPE_P(phptype_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, index + 1) { throw ss::SSException(); } - CHECK_CUSTOM_ERROR( !Z_ISREF_P( var_or_val ) && ( direction == SQL_PARAM_OUTPUT || direction == SQL_PARAM_INPUT_OUTPUT ), stmt, SS_SQLSRV_ERROR_PARAM_VAR_NOT_REF, index + 1 ) { + sqlsrv_phptype srv_phptype; + srv_phptype.value = Z_LVAL_P(phptype_z); + CHECK_CUSTOM_ERROR(!is_valid_sqlsrv_phptype(srv_phptype), stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, index + 1) { throw ss::SSException(); } - } - else { - direction = SQL_PARAM_INPUT; - } - - // extract the php type and encoding from the 3rd parameter - if ( zend_hash_move_forward_ex( param_ht, &pos ) == SUCCESS && ( temp = zend_hash_get_current_data_ex( param_ht, &pos )) != NULL && - Z_TYPE_P( temp ) != IS_NULL ) { - - php_type_param_was_null = false; - sqlsrv_phptype sqlsrv_phptype; - - CHECK_CUSTOM_ERROR( Z_TYPE_P( temp ) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, index + 1 ) { - - throw ss::SSException(); - } - - sqlsrv_phptype.value = Z_LVAL_P( temp ); - - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_phptype ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, - index + 1 ) { - - throw ss::SSException(); - } - - php_out_type = static_cast( sqlsrv_phptype.typeinfo.type ); - encoding = ( SQLSRV_ENCODING ) sqlsrv_phptype.typeinfo.encoding; + php_out_type = static_cast(srv_phptype.typeinfo.type); + encoding = (SQLSRV_ENCODING)srv_phptype.typeinfo.encoding; // if the call has a SQLSRV_PHPTYPE_STRING/STREAM('default'), then the stream is in the encoding established // by the connection - if( encoding == SQLSRV_ENCODING_DEFAULT ) { - encoding = stmt->conn->encoding(); - } - } - // set default for php type and encoding if not supplied - else { - - php_type_param_was_null = true; - - if ( Z_ISREF_P( var_or_val )){ - php_out_type = zend_to_sqlsrv_phptype[Z_TYPE_P( Z_REFVAL_P( var_or_val ))]; - } - else{ - php_out_type = zend_to_sqlsrv_phptype[Z_TYPE_P( var_or_val )]; - } - encoding = stmt->encoding(); - if( encoding == SQLSRV_ENCODING_DEFAULT ) { + if (encoding == SQLSRV_ENCODING_DEFAULT) { encoding = stmt->conn->encoding(); } } - // get the server type, column size/precision and the decimal digits if provided - if ( zend_hash_move_forward_ex( param_ht, &pos ) == SUCCESS && ( temp = zend_hash_get_current_data_ex( param_ht, &pos )) != NULL && - Z_TYPE_P( temp ) != IS_NULL ) { - - sql_type_param_was_null = false; - - CHECK_CUSTOM_ERROR( Z_TYPE_P( temp ) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, index + 1 ) { - + if (sql_type_param_is_null) { + // the sql type is not specified, which is required for always encrypted for non-prepared statements + CHECK_CUSTOM_ERROR(stmt->conn->ce_option.enabled && !stmt->prepared, stmt, SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED) { + throw ss::SSException(); + } + } else { + CHECK_CUSTOM_ERROR(Z_TYPE_P(sqltype_z) != IS_LONG, stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, index + 1) { throw ss::SSException(); } - - sqlsrv_sql_type.value = Z_LVAL_P( temp ); // since the user supplied this type, make sure it's valid - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_sqltype( sqlsrv_sql_type ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, - index + 1 ) { - + sqlsrv_sqltype sqlsrv_sql_type; + sqlsrv_sql_type.value = Z_LVAL_P(sqltype_z); + CHECK_CUSTOM_ERROR(!is_valid_sqlsrv_sqltype(sqlsrv_sql_type), stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, index + 1) { throw ss::SSException(); } - bool size_okay = determine_column_size_or_precision( stmt, sqlsrv_sql_type, &column_size, &decimal_digits ); - - CHECK_CUSTOM_ERROR( !size_okay, stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_PRECISION, index + 1 ) { - + bool size_okay = determine_column_size_or_precision(stmt, sqlsrv_sql_type, &column_size, &decimal_digits); + CHECK_CUSTOM_ERROR(!size_okay, stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_PRECISION, index + 1) { throw ss::SSException(); } sql_type = sqlsrv_sql_type.typeinfo.type; - } - // else the sql type and size are unknown, so tell the core layer to use its defaults - else { - CHECK_CUSTOM_ERROR( !stmt->prepared && stmt->conn->ce_option.enabled, stmt, SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED ) { - throw ss::SSException(); + + if (direction != SQL_PARAM_INPUT && php_type_param_is_null) { + sqlsrv_phptype srv_phptype; + srv_phptype = determine_sqlsrv_php_type(stmt, sql_type, (SQLUINTEGER)column_size, true); + + php_out_type = static_cast(srv_phptype.typeinfo.type); + encoding = static_cast(srv_phptype.typeinfo.encoding); } - sql_type_param_was_null = true; - - sql_type = SQL_UNKNOWN_TYPE; - column_size = SQLSRV_UNKNOWN_SIZE; - decimal_digits = 0; } - // if the user for some reason provides an inout / output parameter with a null phptype and a specified - // sql server type, infer the php type from the sql server type. - if( direction != SQL_PARAM_INPUT && php_type_param_was_null && !sql_type_param_was_null ) { - - sqlsrv_phptype sqlsrv_phptype; - - sqlsrv_phptype = determine_sqlsrv_php_type( stmt, sql_type, (SQLUINTEGER)column_size, true ); - - // we DIE here since everything should have been validated already and to return the user an error - // for our own logic error would be confusing/misleading. - SQLSRV_ASSERT( sqlsrv_phptype.typeinfo.type != PHPTYPE_INVALID, "An invalid php type was returned with (supposed) " - "validated sql type and column_size" ); - - php_out_type = static_cast( sqlsrv_phptype.typeinfo.type ); - encoding = static_cast( sqlsrv_phptype.typeinfo.encoding ); - } - - // verify that the parameter is a valid output param type - if( direction == SQL_PARAM_OUTPUT ) { - - switch( php_out_type ) { - case SQLSRV_PHPTYPE_NULL: - case SQLSRV_PHPTYPE_DATETIME: - case SQLSRV_PHPTYPE_STREAM: - THROW_CORE_ERROR( stmt, SS_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE ); - break; - default: - break; + if (direction == SQL_PARAM_OUTPUT) { + if (php_out_type == SQLSRV_PHPTYPE_NULL || php_out_type == SQLSRV_PHPTYPE_DATETIME || php_out_type == SQLSRV_PHPTYPE_STREAM) { + THROW_CORE_ERROR(stmt, SS_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE); } - } - } - catch( core::CoreException& ) { - - SQLFreeStmt( stmt->handle(), SQL_RESET_PARAMS ); - throw; - } + return var_or_val; } bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) @@ -2079,6 +2022,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) case SQLSRV_PHPTYPE_INT: case SQLSRV_PHPTYPE_FLOAT: case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_TABLE: return true; case SQLSRV_PHPTYPE_STRING: case SQLSRV_PHPTYPE_STREAM: @@ -2123,6 +2067,7 @@ bool is_valid_sqlsrv_sqltype( _In_ sqlsrv_sqltype sql_type ) case SQL_TYPE_DATE: case SQL_SS_TIME2: case SQL_SS_TIMESTAMPOFFSET: + case SQL_SS_TABLE: break; default: return false; diff --git a/source/sqlsrv/template.rc b/source/sqlsrv/template.rc index a44a5238..4d8bcc32 100644 --- a/source/sqlsrv/template.rc +++ b/source/sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index bb57c2d2..f7f50f9d 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.9 for PHP for SQL Server +// Microsoft Drivers 5.10 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -379,7 +379,7 @@ ss_error SS_ERRORS[] = { }, { SQLSRV_ERROR_CE_DRIVER_REQUIRED, - { IMSSP, (SQLCHAR*) "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server.", -105, false } + { IMSSP, (SQLCHAR*) "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server (or above) for %1!s!.", -105, true } }, { SQLSRV_ERROR_CONNECT_INVALID_DRIVER, @@ -445,6 +445,42 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -121, true} }, + { + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + { IMSSP, (SQLCHAR*) "An error occurred translating a string for Table-Valued Param %1!d! Column %2!d! to UTF-16: %3!s!", -122, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + { IMSSP, (SQLCHAR*) "An invalid type for Table-Valued Param %1!d! Column %2!d! was specified", -123, true } + }, + { + SQLSRV_ERROR_TVP_FETCH_METADATA, + { IMSSP, (SQLCHAR*) "Failed to get metadata for Table-Valued Param %1!d!", -124, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_INPUTS, + { IMSSP, (SQLCHAR*) "Invalid inputs for Table-Valued Param %1!d!", -125, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + { IMSSP, (SQLCHAR*) "Expect a non-empty string for a Type Name for Table-Valued Param %1!d!", -126, true } + }, + { + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + { IMSSP, (SQLCHAR*) "For Table-Valued Param %1!d! the number of values in a row is expected to be %2!d!", -127, true } + }, + { + SQLSRV_ERROR_TVP_STRING_KEYS, + { IMSSP, (SQLCHAR*) "Associative arrays not allowed for Table-Valued Param %1!d!", -128, true } + }, + { + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + { IMSSP, (SQLCHAR*) "Expect an array for each row for Table-Valued Param %1!d!", -129, true } + }, + { + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, + { IMSSP, (SQLCHAR*) "You cannot return data in a table-valued parameter. Table-valued parameters are input-only.", -130, false } + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/bvt/pdo_sqlsrv/break.inc b/test/bvt/pdo_sqlsrv/break.inc deleted file mode 100644 index 0540dac8..00000000 --- a/test/bvt/pdo_sqlsrv/break.inc +++ /dev/null @@ -1,60 +0,0 @@ - \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/break_pdo.php b/test/bvt/pdo_sqlsrv/break_pdo.php deleted file mode 100644 index 6082387f..00000000 --- a/test/bvt/pdo_sqlsrv/break_pdo.php +++ /dev/null @@ -1,82 +0,0 @@ -query( $sql ); - - // Insert data - $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 ); - $stmt->execute( $params ); - } - - // Create table - $sql = "CREATE TABLE $tableName2 ( c1 INT, c2 VARCHAR(40) )"; - $stmt = $conn->query( $sql ); - - // Insert data - $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 ); - $stmt->execute( $params ); - } - - $conn = null; -} - -// 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 ) -{ - $stmt1 = $conn->query( "SELECT @@SPID" ); - $obj = $stmt1->fetch( PDO::FETCH_NUM ); - $spid = $obj[0]; - - $stmt2 = $conn_break->query( "KILL ".$spid ); - sleep(1); -} - -// Remove any databases previously created by GenerateDatabase -function DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ) -{ - $conn = new PDO( "sqlsrv:server = $server ; ", $uid, $pwd ); - - $query="IF OBJECT_ID('tempdb.dbo.$tableName1', 'U') IS NOT NULL DROP TABLE tempdb.dbo.$tableName1"; - $stmt=$conn->query( $query ); - - $query="IF OBJECT_ID('tempdb.dbo.$tableName2', 'U') IS NOT NULL DROP TABLE tempdb.dbo.$tableName2"; - $stmt=$conn->query( $query ); -} - -DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ); -GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ); - -?> \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/connect.inc b/test/bvt/pdo_sqlsrv/connect.inc index a6bd479f..470b5cb8 100644 --- a/test/bvt/pdo_sqlsrv/connect.inc +++ b/test/bvt/pdo_sqlsrv/connect.inc @@ -3,6 +3,19 @@ $server = 'TARGET_SERVER'; $databaseName = 'TARGET_DATABASE'; $uid = 'TARGET_USERNAME'; $pwd = 'TARGET_PASSWORD'; + +function dropTable($conn, $tableName) +{ + $tsql = "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" . $tableName . "') AND type in (N'U')) DROP TABLE [$tableName]"; + $conn->exec($tsql); +} + +function dropProc($conn, $procName) +{ + $tsql = "IF OBJECT_ID('". $procName ."', 'P') IS NOT NULL DROP PROCEDURE [$procName]"; + $conn->exec($tsql); +} + ?> diff --git a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_bindParam_3.phpt b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_bindParam_3.phpt index a409e645..df081c8d 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_bindParam_3.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_bindParam_3.phpt @@ -7,17 +7,19 @@ uses an input/output parameter require('connect.inc'); $dbh = new PDO( "sqlsrv:server=$server ; Database = $databaseName", "$uid", "$pwd"); - $dbh->query("IF OBJECT_ID('dbo.sp_ReverseString', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_ReverseString"); - $dbh->query("CREATE PROCEDURE dbo.sp_ReverseString @String as VARCHAR(2048) OUTPUT as SELECT @String = REVERSE(@String)"); - $stmt = $dbh->prepare("EXEC dbo.sp_ReverseString ?"); + dropProc($dbh, 'sp_ReverseString'); + $dbh->query("CREATE PROCEDURE sp_ReverseString @String as VARCHAR(2048) OUTPUT as SELECT @String = REVERSE(@String)"); + $stmt = $dbh->prepare("EXEC sp_ReverseString ?"); $string = "123456789"; $stmt->bindParam(1, $string, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 2048); $stmt->execute(); print "Result: ".$string; // Expect 987654321 + dropProc($dbh, 'sp_ReverseString', false); + //free the statement and connection - $stmt = null; - $dbh = null; + unset($stmt); + unset($dbh); ?> --EXPECT-- Result: 987654321 \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_debugDumpParams.phpt b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_debugDumpParams.phpt index b4b21562..e1dccd2b 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_debugDumpParams.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_debugDumpParams.phpt @@ -23,20 +23,20 @@ $stmt->debugDumpParams(); $stmt=null; $conn=null; ?> ---EXPECT-- -SQL: [52] select * from Person.ContactType where name = :param +--EXPECTREGEX-- +SQL: \[52\] select \* from Person.ContactType where name = \:param Params: 1 -Key: Name: [6] :param +Key: Name: \[6\] :param paramno=0 -name=[6] ":param" +name=\[6\] ":param" is_param=1 -param_type=2 +param_type=(2|3) -SQL: [47] select * from Person.ContactType where name = ? +SQL: \[47\] select \* from Person.ContactType where name = \? Params: 1 Key: Position #0: paramno=0 -name=[0] "" +name=\[0\] "" is_param=1 -param_type=2 \ No newline at end of file +param_type=(2|3) \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_getColumnMeta.phpt b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_getColumnMeta.phpt index e907a6da..d40209e3 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_getColumnMeta.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_getColumnMeta.phpt @@ -19,24 +19,24 @@ print $metadata['name']; $stmt = null; $conn = null; ?> ---EXPECT-- -array(8) { - ["flags"]=> - int(0) - ["sqlsrv:decl_type"]=> - string(8) "datetime" - ["native_type"]=> - string(6) "string" - ["table"]=> - string(0) "" - ["pdo_type"]=> - int(2) - ["name"]=> - string(12) "ModifiedDate" - ["len"]=> - int(23) - ["precision"]=> - int(3) +--EXPECTREGEX-- +array\(8\) { + \["flags"\]=> + int\(0\) + \["sqlsrv:decl_type"\]=> + string\(8\) "datetime" + \["native_type"\]=> + string\(6\) "string" + \["table"\]=> + string\(0\) "" + \["pdo_type"\]=> + int\((2|3)\) + \["name"\]=> + string\(12\) "ModifiedDate" + \["len"\]=> + int\(23\) + \["precision"\]=> + int\(3\) } datetime string diff --git a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_rowCount.phpt b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_rowCount.phpt index 975a6264..3ab74fac 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdoStatement_rowCount.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdoStatement_rowCount.phpt @@ -5,30 +5,33 @@ returns the number of rows added to a table; returns the number of rows in a res --FILE-- exec("CREAtE TABLE Table1(col1 VARCHAR(15), col2 VARCHAR(15)) "); +$conn = new PDO("sqlsrv:Server=$server; Database = $databaseName", $uid, $pwd); + +$tableName = "pdoRowCount"; +dropTable($conn, $tableName); +$conn->exec("CREATE TABLE $tableName(col1 VARCHAR(15), col2 VARCHAR(15)) "); $col1 = 'a'; $col2 = 'b'; -$query = "insert into Table1(col1, col2) values(?, ?)"; +$query = "INSERT INTO $tableName(col1, col2) values(?, ?)"; $stmt = $conn->prepare( $query ); $stmt->execute( array( $col1, $col2 ) ); print $stmt->rowCount(); -print " rows affects."; +print " rows affected."; echo "\n\n"; //revert the insert -$conn->exec("delete from Table1 where col1 = 'a' AND col2 = 'b'"); +$conn->exec("DELETE FROM $tableName where col1 = 'a' AND col2 = 'b'"); -$conn->exec("DROP TABLE Table1 "); +dropTable($conn, $tableName, false); $conn = null; -$conn = new PDO( "sqlsrv:server=$server ; Database = $databaseName", "$uid", "$pwd"); +$conn = new PDO("sqlsrv:Server=$server; Database = $databaseName", $uid, $pwd); -$query = "select * from Person.ContactType"; +$query = "SELECT * FROM Person.ContactType"; $stmt = $conn->prepare( $query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); $stmt->execute(); print $stmt->rowCount(); @@ -40,6 +43,6 @@ $stmt = null; $conn = null; ?> --EXPECT-- -1 rows affects. +1 rows affected. 20 rows in result set. \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_beginTransaction.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_beginTransaction.phpt index edc0632f..568d6d1f 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_beginTransaction.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_beginTransaction.phpt @@ -4,27 +4,30 @@ starts a transaction, insert 2 rows and commit the transaction --FILE-- exec("CREAtE TABLE Table1(col1 CHARACTER(1), col2 CHARACTER(1)) "); - - $conn->beginTransaction(); - $ret = $conn->exec("insert into Table1(col1, col2) values('a', 'b') "); - $ret = $conn->exec("insert into Table1(col1, col2) values('a', 'c') "); - - //revert the inserts - $ret = $conn->exec("delete from Table1 where col1 = 'a'"); - $conn->commit(); - // $conn->rollback(); - echo $ret." rows affected"; - - //drop the created temp table - $conn->exec("DROP TABLE Table1 "); - - //free statement and connection - $ret=NULL; - $conn=NULL; + require('connect.inc'); + + //make connection and create a temporaty table + $conn = new PDO( "sqlsrv:Server=$server; Database = $databaseName ", "$uid", "$pwd"); + $tableName = "pdoBeginTransaction"; + dropTable($conn, $tableName); + + $conn->exec("CREATE TABLE $tableName(col1 CHARACTER(1), col2 CHARACTER(1)) "); + + $conn->beginTransaction(); + $ret = $conn->exec("insert into $tableName(col1, col2) values('a', 'b') "); + $ret = $conn->exec("insert into $tableName(col1, col2) values('a', 'c') "); + + //revert the inserts + $ret = $conn->exec("delete from $tableName where col1 = 'a'"); + $conn->commit(); + + // $conn->rollback(); + echo $ret." rows affected"; + + //drop the created temp table + dropTable($conn, $tableName, false); + + unset($conn); ?> --EXPECT-- 2 rows affected \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_construct_2.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_construct_2.phpt index 5f937210..e7a74a08 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_construct_2.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_construct_2.phpt @@ -5,9 +5,8 @@ connect to a server, specifying the database later --FILE-- exec( "USE $databaseName"); $query = 'SELECT * FROM Person.ContactType'; $stmt = $c->query( $query ); while ( $row = $stmt->fetch( PDO::FETCH_ASSOC ) ){ diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_exec.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_exec.phpt index 2f0f9b12..3def85a9 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_exec.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_exec.phpt @@ -4,21 +4,22 @@ execute a delete and reports how many rows were deleted --FILE-- exec("use tempdb"); - $c->exec("CREAtE TABLE Table1(col1 VARCHAR(100), col2 VARCHAR(100)) "); - - $ret = $c->exec("insert into Table1 values('xxxyy', 'yyxx')"); - $ret = $c->exec("delete from Table1 where col1 = 'xxxyy'"); - echo $ret," rows affected"; - - $c->exec("DROP TABLE Table1 "); - - //free the statement and connection - $ret=null; - $c=null; + $tableName = "pdoExec"; + dropTable($c, $tableName); + + $c->exec("CREATE TABLE $tableName(col1 VARCHAR(100), col2 VARCHAR(100)) "); + + $ret = $c->exec("INSERT INTO $tableName VALUES('xxxyy', 'yyxx')"); + $ret = $c->exec("DELETE FROM $tableName WHERE col1 = 'xxxyy'"); + echo $ret," rows affected"; + + dropTable($c, $tableName, false); + + //free the statement and connection + unset($c); ?> --EXPECT-- 1 rows affected \ No newline at end of file diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_prepare.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_prepare.phpt index 6b192631..935b4935 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_prepare.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_prepare.phpt @@ -5,31 +5,34 @@ prepares a statement with parameter markers and forward-only (server-side) curso --FILE-- exec("CREAtE TABLE Table1(col1 VARCHAR(100), col2 VARCHAR(100))"); +$tableName = "pdoPrepare"; +dropTable($conn, $tableName); + +$conn->exec("CREATE TABLE $tableName(col1 VARCHAR(100), col2 VARCHAR(100))"); $col1 = 'a'; $col2 = 'b'; -$query = "insert into Table1(col1, col2) values(?, ?)"; +$query = "INSERT INTO $tableName(col1, col2) VALUES(?, ?)"; $stmt = $conn->prepare( $query, array( PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 1 ) ); $stmt->execute( array( $col1, $col2 ) ); print $stmt->rowCount(); echo " row affected\n"; -$query = "insert into Table1(col1, col2) values(:col1, :col2)"; +$query = "INSERT INTO $tableName(col1, col2) VALUES(:col1, :col2)"; $stmt = $conn->prepare( $query, array( PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 1 ) ); $stmt->execute( array( ':col1' => $col1, ':col2' => $col2 ) ); print $stmt->rowCount(); echo " row affected\n"; // revert the inserts -$conn->exec("delete from Table1 where col1 = 'a' AND col2 = 'b'"); +$conn->exec("DELETE FROM $tableName WHERE col1 = 'a' AND col2 = 'b'"); +dropTable($conn, $tableName, false); -$conn->exec("DROP TABLE Table1 "); -$stmt = null; -$conn = null; +unset($stmt); +unset($conn); ?> --EXPECT-- 1 row affected diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_quote.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_quote.phpt index 6bd10956..26e1ec05 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_quote.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_quote.phpt @@ -6,36 +6,40 @@ insert with quoted parameters exec("CREAtE TABLE Table1(col1 VARCHAR(15), col2 VARCHAR(15)) "); +$conn = new PDO("sqlsrv:Server=$server; Database = $databaseName", $uid, $pwd); + +$tableName = "pdoQuote"; +dropTable($conn, $tableName); + +$conn->exec("CREATE TABLE $tableName(col1 VARCHAR(15), col2 VARCHAR(15)) "); $param = 'a \' g'; $param2 = $conn->quote( $param ); -$query = "INSERT INTO Table1 VALUES( ?, '1' )"; +$query = "INSERT INTO $tableName VALUES( ?, '1' )"; $stmt = $conn->prepare( $query ); $stmt->execute(array($param)); -$query = "INSERT INTO Table1 VALUES( ?, ? )"; +$query = "INSERT INTO $tableName VALUES( ?, ? )"; $stmt = $conn->prepare( $query ); $stmt->execute(array($param, $param2)); -$query = "SELECT * FROM Table1"; +$query = "SELECT * FROM $tableName"; $stmt = $conn->query($query); while ( $row = $stmt->fetch( PDO::FETCH_ASSOC ) ){ print_r( $row['col1'] ." was inserted\n" ); } // revert the inserts -$query = "delete from Table1 where col1 = ?"; +$query = "DELETE FROM $tableName WHERE col1 = ?"; $stmt = $conn->prepare( $query ); $stmt->execute(array($param)); -$conn->exec("DROP TABLE Table1 "); - +dropTable($conn, $tableName, false); + //free the statement and connection -$stmt=null; -$conn=null; +unset($stmt); +unset($conn); ?> --EXPECT-- a ' g was inserted diff --git a/test/bvt/pdo_sqlsrv/msdn_pdo_setAttribute_direct_query.phpt b/test/bvt/pdo_sqlsrv/msdn_pdo_setAttribute_direct_query.phpt index f78c59ba..3b940739 100644 --- a/test/bvt/pdo_sqlsrv/msdn_pdo_setAttribute_direct_query.phpt +++ b/test/bvt/pdo_sqlsrv/msdn_pdo_setAttribute_direct_query.phpt @@ -4,27 +4,24 @@ sets to PDO::SQLSRV_ATTR_DIRECT_QUERY --FILE-- setAttribute(constant('PDO::SQLSRV_ATTR_DIRECT_QUERY'), true); + require('connect.inc'); + $conn = new PDO("sqlsrv:Server=$server; Database = $databaseName", $uid, $pwd); + $conn->setAttribute(PDO::SQLSRV_ATTR_DIRECT_QUERY, true); $tableName = 'pdo_direct_query'; - $tsql = "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" . $tableName . "') AND type in (N'U')) DROP TABLE $tableName"; - - $stmt1 = $conn->query($tsql); - $stmt2 = $conn->query("CREATE TABLE $tableName ([c1_int] int, [c2_int] int)"); + $conn->query("CREATE TABLE $tableName ([c1_int] int, [c2_int] int)"); $v1 = 1; $v2 = 2; - $stmt3 = $conn->prepare("INSERT INTO $tableName (c1_int, c2_int) VALUES (:var1, :var2)"); + $stmt = $conn->prepare("INSERT INTO $tableName (c1_int, c2_int) VALUES (:var1, :var2)"); - if ($stmt3) { - $stmt3->bindValue(1, $v1); - $stmt3->bindValue(2, $v2); + if ($stmt) { + $stmt->bindValue(1, $v1); + $stmt->bindValue(2, $v2); - if ($stmt3->execute()) { - echo "Execution succeeded\n"; + if ($stmt->execute()) { + echo "Execution succeeded\n"; } else { echo "Execution failed\n"; } @@ -32,13 +29,10 @@ sets to PDO::SQLSRV_ATTR_DIRECT_QUERY var_dump($conn->errorInfo()); } - $stmt4 = $conn->query("DROP TABLE $tableName"); + $stmt = $conn->query("DROP TABLE $tableName"); // free the statements and connection - unset($stmt1); - unset($stmt2); - unset($stmt3); - unset($stmt4); + unset($stmt); unset($conn); ?> --EXPECT-- diff --git a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_double.phpt b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_double.phpt index f82871f3..11919573 100644 --- a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_double.phpt +++ b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_double.phpt @@ -7,11 +7,7 @@ call a stored procedure and retrieve the errorNumber that is returned require('connect.inc'); $conn = new PDO( "sqlsrv:server=$server ; Database = $databaseName", "$uid", "$pwd"); - // Drop the stored procedure if it already exists - $tsql_dropSP = "IF OBJECT_ID('sp_Test_Double', 'P') IS NOT NULL - DROP PROCEDURE sp_Test_Double"; - - $stmt = $conn->query($tsql_dropSP); + dropProc($conn, 'sp_Test_Double'); // Create the stored procedure $tsql_createSP = "CREATE PROCEDURE sp_Test_Double @@ -39,10 +35,11 @@ call a stored procedure and retrieve the errorNumber that is returned print("Error Number minus 2: $value\n\n"); print_r($result); - + dropProc($conn, 'sp_Test_Double', false); + //free the statement and connection - $stmt = null; - $conn = null; + unset($stmt); + unset($conn); ?> --EXPECT-- Error Number: -1.111 diff --git a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_integer.phpt b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_integer.phpt index 57135fe9..2a882acb 100644 --- a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_integer.phpt +++ b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_integer.phpt @@ -4,43 +4,43 @@ call a stored procedure and retrieve the errorNumber that is returned --FILE-- query($tsql_dropSP); + // Drop the stored procedure if it already exists + dropProc($conn, 'sp_Test_Integer'); - // Create the stored procedure - $tsql_createSP = "CREATE PROCEDURE sp_Test_Integer - @ErrorNumber AS INT = 0 OUTPUT - AS - BEGIN - SET @ErrorNumber = -1 - SELECT 1,2,3 - END"; + // Create the stored procedure + $tsql_createSP = "CREATE PROCEDURE sp_Test_Integer + @ErrorNumber AS INT = 0 OUTPUT + AS + BEGIN + SET @ErrorNumber = -1 + SELECT 1,2,3 + END"; - $stmt = $conn->query($tsql_createSP); + $stmt = $conn->query($tsql_createSP); - // Call the stored procedure - $stmt = $conn->prepare("{CALL sp_Test_Integer (:errornumber)}"); + // Call the stored procedure + $stmt = $conn->prepare("{CALL sp_Test_Integer (:errornumber)}"); - $errorNumber = 0; - $stmt->bindParam('errornumber', $errorNumber, PDO::PARAM_INT|PDO::PARAM_INPUT_OUTPUT, 4); + $errorNumber = 0; + $stmt->bindParam('errornumber', $errorNumber, PDO::PARAM_INT|PDO::PARAM_INPUT_OUTPUT, 4); - $stmt->execute(); - $result = $stmt->fetchAll(PDO::FETCH_NUM); + $stmt->execute(); + $result = $stmt->fetchAll(PDO::FETCH_NUM); - $stmt->closeCursor(); + $stmt->closeCursor(); - print("Error Number: $errorNumber\n\n"); - print_r($result); - - //free the statement and connection - $stmt = null; - $conn = null; + print("Error Number: $errorNumber\n\n"); + print_r($result); + + // Drop the stored procedure + dropProc($conn, 'sp_Test_Integer', false); + + //free the statement and connection + unset($stmt); + unset($conn); ?> --EXPECT-- Error Number: -1 diff --git a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_string.phpt b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_string.phpt index f2743392..5899419e 100644 --- a/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_string.phpt +++ b/test/bvt/pdo_sqlsrv/pdo_bindParam_inout_string.phpt @@ -4,44 +4,43 @@ call a stored procedure and retrieve the errorString that is returned --FILE-- query($tsql_dropSP); + // Create the stored procedure + $tsql_createSP = "CREATE PROCEDURE sp_Test_String + @ErrorString as varchar(20) OUTPUT + AS + BEGIN + SET @ErrorString = REVERSE(@ErrorString) + SELECT 1,2,3 + END"; + $stmt = $conn->query($tsql_createSP); - // Create the stored procedure - $tsql_createSP = "CREATE PROCEDURE sp_Test_String - @ErrorString as varchar(20) OUTPUT - AS - BEGIN - SET @ErrorString = REVERSE(@ErrorString) - SELECT 1,2,3 - END"; - $stmt = $conn->query($tsql_createSP); + // Call the stored procedure + $stmt = $conn->prepare("{CALL sp_Test_String (?)}"); - // Call the stored procedure - $stmt = $conn->prepare("{CALL sp_Test_String (?)}"); + $errorString = "12345"; + $stmt->bindParam(1, $errorString, PDO::PARAM_STR|PDO::PARAM_INPUT_OUTPUT, 20); + print("Error String: $errorString\n\n"); - $errorString = "12345"; - $stmt->bindParam(1, $errorString, PDO::PARAM_STR|PDO::PARAM_INPUT_OUTPUT, 20); - print("Error String: $errorString\n\n"); + $stmt->execute(); - $stmt->execute(); + $result = $stmt->fetchAll(PDO::FETCH_NUM); - $result = $stmt->fetchAll(PDO::FETCH_NUM); + $stmt->closeCursor(); + + print("Error String: $errorString\n\n"); + print_r($result); + + dropProc($conn, 'sp_Test_String', false); - $stmt->closeCursor(); - - print("Error String: $errorString\n\n"); - print_r($result); - - //free the statement and connection - $stmt = null; - $conn = null; + //free the statement and connection + unset($stmt); + unset($conn); ?> --EXPECT-- Error String: 12345 diff --git a/test/bvt/sqlsrv/break.inc b/test/bvt/sqlsrv/break.inc deleted file mode 100644 index 5e6429f7..00000000 --- a/test/bvt/sqlsrv/break.inc +++ /dev/null @@ -1,109 +0,0 @@ -"$databaseName", "username"=>"$username", "password"=>"$password" ); -$conn = sqlsrv_connect( $serverName, $connectionInfo ); - -// CREATE database -$stmt0 = sqlsrv_query($conn, "CREATE DATABASE $databaseName"); - -// Create table -$sql = "CREATE TABLE $tableName1 (c1 INT, c2 VARCHAR(40))"; -$stmt = sqlsrv_query($conn, $sql); - -// Insert data using bind parameters -$sql = "INSERT INTO $tableName1 VALUES (?,?)"; -for($t=100; $t<115; $t++) { - $stmt = sqlsrv_prepare($conn, $sql); - $ts = substr(sha1($t),0,5); - $params = array($t,$ts); - sqlsrv_execute($stmt, $params); -} - -// Create table -$sql = "CREATE TABLE $tableName2 (c1 INT, c2 VARCHAR(40))"; -$stmt = sqlsrv_query($conn, $sql); - -// Insert data using bind parameters -$sql = "INSERT INTO $tableName2 VALUES (?,?)"; -for($t=200; $t<208; $t++) { - $stmt = sqlsrv_prepare($conn, $sql); - $ts = substr(sha1($t),0,5); - $params = array($t,$ts); - sqlsrv_execute($stmt, $params); -} - -sqlsrv_close( $conn ); - -function RestartConn($serverName) -{ - $powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; - $restart_string = "$powershell (get-service -ComputerName $serverName -Name mssqlserver).Stop()"; - exec( $restart_string ); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - - // Wait until the service is fully stopped - while (substr_count($servstring, "Stopped") != 1) - { - sleep(1); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - } - $restart_string = "$powershell (get-service -ComputerName $serverName -Name mssqlserver).Start()"; - exec( $restart_string ); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - - // Wait until the service is fully started - while (substr_count($servstring, "Running") != 1) - { - sleep(1); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - } -} - -function StopConn($serverName) -{ - $powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; - $restart_string = "$powershell (get-service -ComputerName $serverName -Name mssqlserver).Stop()"; - exec( $restart_string ); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - - // Wait until the service is fully stopped - while (substr_count($servstring, "Stopped") != 1) - { - sleep(1); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - } -} - -function StartConn($serverName) -{ - $powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - if (substr_count($servstring, "Running") != 1) - { - $restart_string = "$powershell (get-service -ComputerName $serverName -Name mssqlserver).Start()"; - exec( $restart_string ); - } - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - - // Wait until the service is fully started - while (substr_count($servstring, "Running") != 1) - { - sleep(1); - $servstring = shell_exec("$powershell get-service -ComputerName $serverName -Name mssqlserver"); - } -} -?> \ No newline at end of file diff --git a/test/bvt/sqlsrv/break.php b/test/bvt/sqlsrv/break.php deleted file mode 100644 index da5826c5..00000000 --- a/test/bvt/sqlsrv/break.php +++ /dev/null @@ -1,88 +0,0 @@ -$dbName, "uid"=>$uid, "pwd"=>$pwd ); - - $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 ); - - // 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 ); - } - - // Create table - $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 ); - } - - 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 ) -{ - $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 ); - sleep(1); -} - -// Remove the tables generated by GenerateTables -function DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ) -{ - $connectionInfo = array( "UID"=>$uid, "PWD"=>$pwd ); - $conn = sqlsrv_connect( $server, $connectionInfo ); - - $query="IF OBJECT_ID('tempdb.dbo.$tableName1, 'U') IS NOT NULL DROP TABLE tempdb.dbo.$tableName1"; - $stmt=sqlsrv_query( $conn, $query ); - - $query="IF OBJECT_ID('tempdb.dbo.$tableName2, 'U') IS NOT NULL DROP TABLE tempdb.dbo.$tableName2"; - $stmt=sqlsrv_query( $conn, $query ); -} - -DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ); -GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ); - -?> \ No newline at end of file diff --git a/test/bvt/sqlsrv/connect.inc b/test/bvt/sqlsrv/connect.inc index 6ac30a9c..71759d9e 100644 --- a/test/bvt/sqlsrv/connect.inc +++ b/test/bvt/sqlsrv/connect.inc @@ -4,30 +4,23 @@ $databaseName = 'TARGET_DATABASE'; $uid = 'TARGET_USERNAME'; $pwd = 'TARGET_PASSWORD'; -$server2 = 'ANOTHER_SERVER'; +function dropTable($conn, $tableName) +{ + $tsql = "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" . $tableName . "') AND type in (N'U')) DROP TABLE [$tableName]"; + sqlsrv_query($conn, $tsql); +} + +function dropProc($conn, $procName) +{ + $tsql = "IF OBJECT_ID('". $procName ."', 'P') IS NOT NULL DROP PROCEDURE [$procName]"; + sqlsrv_query($conn, $tsql); +} // 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 -// before it can overflow. -function ResetRevisionNumber( $server, $databaseName, $uid, $pwd ) -{ - $connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd ); - $conn = sqlsrv_connect( $server, $connectionInfo); - if( $conn === false ) - { - echo "Could not connect.
"; - die( print_r( sqlsrv_errors(), true)); - } - - $stmt0 = sqlsrv_query( $conn, "UPDATE Sales.SalesOrderHeader SET RevisionNumber = 2"); - if ( !$stmt0 ) - { - echo "Resetting the RevisionNumber failed.\n"; - die( print_r( sqlsrv_errors(), true)); - } - - sqlsrv_close( $conn ); +// overflow quickly if the BVT tests often run. Disable the trigger. +function disableTrigger($conn) +{ + sqlsrv_query($conn, 'DISABLE TRIGGER uSalesOrderHeader ON Sales.SalesOrderHeader'); } - ?> diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_begin_transaction.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_begin_transaction.phpt index 399f7a73..742d01d3 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_begin_transaction.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_begin_transaction.phpt @@ -6,9 +6,6 @@ executes two queries as part of a transaction "$databaseName", "UID"=>$uid, "PWD"=>$pwd); $conn = sqlsrv_connect( $server, $connectionInfo); if( $conn === false ) @@ -17,6 +14,8 @@ if( $conn === false ) die( print_r( sqlsrv_errors(), true )); } +disableTrigger($conn); + /* Initiate transaction. */ /* Exit script if transaction cannot be initiated. */ if ( sqlsrv_begin_transaction( $conn ) === false ) diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_configure_2.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_configure_2.phpt index 86cbd3b4..540d2fae 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_configure_2.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_configure_2.phpt @@ -34,26 +34,15 @@ $r_sql="UPDATE HumanResources.Employee SET VacationHours=61 WHERE BusinessEntity UPDATE HumanResources.Employee SET VacationHours=62 WHERE BusinessEntityID=8; UPDATE HumanResources.Employee SET VacationHours=63 WHERE BusinessEntityID=9; UPDATE HumanResources.Employee SET VacationHours=7 WHERE BusinessEntityID=11"; -$stmt4=sqlsrv_query($conn, $r_sql); -sqlsrv_free_stmt( $stmt4 ); +$stmt=sqlsrv_query($conn, $r_sql); +sqlsrv_free_stmt( $stmt ); /* Drop the stored procedure if it already exists. */ -$tsql1 = "IF OBJECT_ID('SubtractVacationHours', 'P') IS NOT NULL - DROP PROCEDURE SubtractVacationHours"; -$stmt1 = sqlsrv_query($conn, $tsql1); +dropProc($conn, "SubtractVacationHours"); -/* If the query fails, display errors and exit the script. */ -if( $stmt1 === false) -{ - DisplayErrors(); - die; -} /* Display any warnings. */ DisplayWarnings(); -/* Free the statement resources. */ -sqlsrv_free_stmt( $stmt1 ); - /* Create the stored procedure. */ $tsql2 = "CREATE PROCEDURE SubtractVacationHours @BusinessEntityId int, @@ -69,10 +58,10 @@ $tsql2 = "CREATE PROCEDURE SubtractVacationHours BEGIN PRINT 'WARNING: Vacation hours are now less than zero.' END;"; -$stmt2 = sqlsrv_query( $conn, $tsql2 ); +$stmt = sqlsrv_query( $conn, $tsql2 ); /* If the query fails, display errors and exit the script. */ -if( $stmt2 === false) +if( $stmt === false) { DisplayErrors(); die; @@ -81,7 +70,7 @@ if( $stmt2 === false) DisplayWarnings(); /* Free the statement resources. */ -sqlsrv_free_stmt( $stmt2 ); +sqlsrv_free_stmt( $stmt ); /* Set up the array that maps employee ID to used vacation hours. */ $emp_hrs = array (7=>4, 8=>5, 9=>8, 11=>50); @@ -98,10 +87,10 @@ $params = array( /* Define and prepare the query to substract used vacation hours. */ $tsql3 = "{call SubtractVacationHours(?, ?)}"; -$stmt3 = sqlsrv_prepare($conn, $tsql3, $params); +$stmt = sqlsrv_prepare($conn, $tsql3, $params); /* If the statement preparation fails, display errors and exit the script. */ -if( $stmt3 === false) +if( $stmt === false) { DisplayErrors(); die; @@ -115,7 +104,7 @@ foreach(array_keys($emp_hrs) as $businessEntityId) { $vacationHrs = $emp_hrs[$businessEntityId]; /* Execute the query. If it fails, display the errors. */ - if( sqlsrv_execute($stmt3) === false) + if( sqlsrv_execute($stmt) === false) { DisplayErrors(); die; @@ -124,7 +113,7 @@ foreach(array_keys($emp_hrs) as $businessEntityId) DisplayWarnings(); /*Move to the next result returned by the stored procedure. */ - if( sqlsrv_next_result($stmt3) === false) + if( sqlsrv_next_result($stmt) === false) { DisplayErrors(); die; @@ -138,7 +127,9 @@ foreach(array_keys($emp_hrs) as $businessEntityId) } /* Free the statement*/ -sqlsrv_free_stmt( $stmt3 ); +sqlsrv_free_stmt( $stmt ); + +dropProc($conn, "SubtractVacationHours", false); /* close connection resources. */ sqlsrv_close( $conn ); @@ -169,10 +160,10 @@ function DisplayWarnings() } ?> --EXPECTREGEX-- -Warning: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]Changed database context to 'AdventureWorks201[4|7]'. +Warning: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]Changed database context to 'AdventureWorks201.'. Warning: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]Changed language setting to us_english. BusinessEntityId 7 has 57 remaining vacation hours. BusinessEntityId 8 has 57 remaining vacation hours. BusinessEntityId 9 has 55 remaining vacation hours. -Error: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]The UPDATE statement conflicted with the CHECK constraint "CK_Employee_VacationHours". The conflict occurred in database "AdventureWorks201[4|7]", table "HumanResources.Employee", column 'VacationHours'. +Error: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]The UPDATE statement conflicted with the CHECK constraint "CK_Employee_VacationHours". The conflict occurred in database "AdventureWorks201.", table "HumanResources.Employee", column 'VacationHours'. Error: \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]The statement has been terminated. \ No newline at end of file diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt index 55d40f4f..e751b7e2 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt @@ -5,17 +5,9 @@ disables MARS support. --FILE-- "$databaseName", "UID"=>"$uid", "PWD"=>"$pwd"); +$connectionInfo = array("Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd, 'MultipleActiveResultSets'=> false); $conn = sqlsrv_connect( $server, $connectionInfo); - -/* Connect to the local server using Windows Authentication and -specify the AdventureWorks database as the database in use. */ -$serverName = $server2; -$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd, 'MultipleActiveResultSets'=> false); - -$conn = sqlsrv_connect( $serverName, $connectionInfo); -if( $conn === false ) -{ +if( $conn === false ) { echo "Could not connect.\n"; die( print_r( sqlsrv_errors(), true)); } diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_execute_string.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_execute_string.phpt index ce173e3e..d4d241a1 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_execute_string.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_execute_string.phpt @@ -57,17 +57,13 @@ echo "
"; sqlsrv_free_stmt( $stmt); +disableTrigger($conn); + /* Prepare with string type in parameter. */ $tsql = "UPDATE Sales.SalesOrderDetail SET OrderQty=(?) WHERE CarrierTrackingNumber=(?)"; -// RevisionNumber in SalesOrderHeader is subject to a trigger incrementing it whenever -// changes are made to SalesOrderDetail. Since RevisonNumber is a tinyint, it can -// overflow quickly if this test is often run. So we change it directly here first -// before it can overflow. -$stmt0 = sqlsrv_query( $conn, "UPDATE Sales.SalesOrderHeader SET RevisionNumber = 2 WHERE SalesOrderID = $soID" ); - //Pass in parameters directly $params = array(5, '8650-4A20-B1'); $stmt = sqlsrv_prepare( $conn, $tsql, $params); @@ -140,11 +136,9 @@ else die( print_r( sqlsrv_errors(), true)); } echo sqlsrv_rows_affected( $stmt)." rows affected.
"; -sqlsrv_free_stmt( $stmt); - /* Free the statement and connection resources. */ -//sqlsrv_free_stmt( $stmt); +sqlsrv_free_stmt( $stmt); sqlsrv_close( $conn); ?> --EXPECT-- diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_next_result.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_next_result.phpt index 92250def..657c3530 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_next_result.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_next_result.phpt @@ -17,17 +17,10 @@ if( $conn === false ) /*revert inserts from previous tests*/ $d_sql = "DELETE FROM Production.ProductReview WHERE EmailAddress!='john@fourthcoffee.com' AND ProductID=709"; -$stmt4 = sqlsrv_query($conn, $d_sql); +$stmt = sqlsrv_query($conn, $d_sql); /* Drop the stored procedure if it already exists. */ -$tsql_dropSP = "IF OBJECT_ID('InsertProductReview', 'P') IS NOT NULL - DROP PROCEDURE InsertProductReview"; -$stmt1 = sqlsrv_query( $conn, $tsql_dropSP); -if( $stmt1 === false ) -{ - echo "Error in executing statement 1.\n"; - die( print_r( sqlsrv_errors(), true)); -} +dropProc($conn, 'InsertProductReview'); /* Create the stored procedure. */ $tsql_createSP = " CREATE PROCEDURE InsertProductReview @@ -56,9 +49,9 @@ $tsql_createSP = " CREATE PROCEDURE InsertProductReview SELECT * FROM Production.ProductReview WHERE ProductID = @ProductID; END"; -$stmt2 = sqlsrv_query( $conn, $tsql_createSP); +$stmt = sqlsrv_query( $conn, $tsql_createSP); -if( $stmt2 === false) +if( $stmt === false) { echo "Error in executing statement 2.\n"; die( print_r( sqlsrv_errors(), true)); @@ -86,8 +79,8 @@ $params = array( ); /* Execute the query. */ -$stmt3 = sqlsrv_query( $conn, $tsql_callSP, $params); -if( $stmt3 === false) +$stmt = sqlsrv_query( $conn, $tsql_callSP, $params); +if( $stmt === false) { echo "Error in executing statement 3.\n"; die( print_r( sqlsrv_errors(), true)); @@ -97,17 +90,17 @@ echo "

"; /* Consume the first result (rows affected by INSERT query in the stored procedure) without calling sqlsrv_next_result. */ -echo "Rows affectd: ".sqlsrv_rows_affected($stmt3)."-----\n"; +echo "Rows affectd: ".sqlsrv_rows_affected($stmt)."-----\n"; echo "

"; /* Move to the next result and display results. */ -$next_result = sqlsrv_next_result($stmt3); +$next_result = sqlsrv_next_result($stmt); if( $next_result ) { echo "

"; echo "\nReview information for product ID ".$productID.".---\n"; - while( $row = sqlsrv_fetch_array( $stmt3, SQLSRV_FETCH_ASSOC)) + while( $row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC)) { echo "
ReviewerName: ".$row['ReviewerName']."\n"; echo "
ReviewDate: ".date_format($row['ReviewDate'], @@ -127,13 +120,10 @@ else die(print_r(sqlsrv_errors(), true)); } +dropProc($conn, 'InsertProductReview', false); + /* Free statement and connection resources. */ -sqlsrv_free_stmt( $stmt1 ); -sqlsrv_free_stmt( $stmt2 ); -sqlsrv_free_stmt( $stmt3 ); -sqlsrv_free_stmt( $stmt4 ); - - +sqlsrv_free_stmt( $stmt ); sqlsrv_close( $conn ); ?> --EXPECT-- diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query.phpt index f3f45f59..55de6d23 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query.phpt @@ -15,6 +15,8 @@ if( $conn === false ) die( print_r( sqlsrv_errors(), true)); } +disableTrigger($conn); + /* Set up the parameterized query. */ $tsql = "INSERT INTO Sales.SalesOrderDetail (SalesOrderID, @@ -29,12 +31,6 @@ $tsql = "INSERT INTO Sales.SalesOrderDetail /* Set parameter values. */ $params = array(75123, 5, 741, 1, 818.70, 0.00); -// RevisionNumber in SalesOrderHeader is subject to a trigger incrementing it whenever -// changes are made to SalesOrderDetail. Since RevisonNumber is a tinyint, it can -// overflow quickly if this test is often run. So we change it directly here first -// before it can overflow. -$stmt0 = sqlsrv_query( $conn, "UPDATE Sales.SalesOrderHeader SET RevisionNumber = 2 WHERE SalesOrderID = $params[0]"); - /* Prepare and execute the query. */ $stmt = sqlsrv_query( $conn, $tsql, $params); if( $stmt ) diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query_param_inout.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query_param_inout.phpt index 227780d0..f0e8e7e7 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query_param_inout.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query_param_inout.phpt @@ -13,14 +13,7 @@ if( $conn === false ) die( print_r( sqlsrv_errors(), true)); } /* Drop the stored procedure if it already exists. */ -$tsql_dropSP = "IF OBJECT_ID('SubtractVacationHours', 'P') IS NOT NULL - DROP PROCEDURE SubtractVacationHours"; -$stmt1 = sqlsrv_query( $conn, $tsql_dropSP); -if( $stmt1 === false ) -{ - echo "Error in executing statement 1.\n"; - die( print_r( sqlsrv_errors(), true)); -} +dropProc($conn, 'SubtractVacationHours'); /* Create the stored procedure. */ $tsql_createSP = "CREATE PROCEDURE SubtractVacationHours @@ -34,8 +27,8 @@ $tsql_createSP = "CREATE PROCEDURE SubtractVacationHours FROM HumanResources.Employee WHERE BusinessEntityID = @BusinessEntityID)"; -$stmt2 = sqlsrv_query( $conn, $tsql_createSP); -if( $stmt2 === false ) +$stmt = sqlsrv_query( $conn, $tsql_createSP); +if( $stmt === false ) { echo "Error in executing statement 2.\n"; die( print_r( sqlsrv_errors(), true)); @@ -62,26 +55,25 @@ $params = array( ); /* Execute the query. */ -$stmt3 = sqlsrv_query( $conn, $tsql_callSP, $params); -if( $stmt3 === false ) +$stmt = sqlsrv_query( $conn, $tsql_callSP, $params); +if( $stmt === false ) { echo "Error in executing statement 3.\n"; die( print_r( sqlsrv_errors(), true)); } /* Display the value of the output parameter $vacationHrs. */ -sqlsrv_next_result($stmt3); +sqlsrv_next_result($stmt); echo "Remaining vacation hours: ".$vacationHrs; /* Revert the update in vacation hours */ $r_sql = "UPDATE HumanResources.Employee SET VacationHours=48 WHERE BusinessEntityID=4"; -$stmt4 = sqlsrv_query($conn, $r_sql); +$stmt = sqlsrv_query($conn, $r_sql); + +dropProc($conn, 'SubtractVacationHours', false); /*Free the statement and connection resources. */ -sqlsrv_free_stmt( $stmt1); -sqlsrv_free_stmt( $stmt2); -sqlsrv_free_stmt( $stmt3); -sqlsrv_free_stmt( $stmt4); +sqlsrv_free_stmt( $stmt); sqlsrv_close( $conn); ?> --EXPECT-- diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query_param_out.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query_param_out.phpt index 3c8696b8..0b1e9dd7 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query_param_out.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query_param_out.phpt @@ -16,14 +16,7 @@ if( $conn === false ) } /* Drop the stored procedure if it already exists. */ -$tsql_dropSP = "IF OBJECT_ID('GetEmployeeSalesYTD', 'P') IS NOT NULL - DROP PROCEDURE GetEmployeeSalesYTD"; -$stmt1 = sqlsrv_query( $conn, $tsql_dropSP); -if( $stmt1 === false ) -{ - echo "Error in executing statement 1.\n"; - die( print_r( sqlsrv_errors(), true)); -} +dropProc($conn, 'GetEmployeeSalesYTD'); /* Create the stored procedure. */ $tsql_createSP = " CREATE PROCEDURE GetEmployeeSalesYTD @@ -35,8 +28,8 @@ $tsql_createSP = " CREATE PROCEDURE GetEmployeeSalesYTD JOIN HumanResources.vEmployee AS e ON e.BusinessEntityID = sp.BusinessEntityID WHERE LastName = @SalesPerson"; -$stmt2 = sqlsrv_query( $conn, $tsql_createSP); -if( $stmt2 === false ) +$stmt = sqlsrv_query( $conn, $tsql_createSP); +if( $stmt === false ) { echo "Error in executing statement 2.\n"; die( print_r( sqlsrv_errors(), true)); @@ -62,8 +55,8 @@ $params = array( ); /* Execute the query. */ -$stmt3 = sqlsrv_query( $conn, $tsql_callSP, $params); -if( $stmt3 === false ) +$stmt = sqlsrv_query( $conn, $tsql_callSP, $params); +if( $stmt === false ) { echo "Error in executing statement 3.\n"; die( print_r( sqlsrv_errors(), true)); @@ -72,10 +65,10 @@ if( $stmt3 === false ) /* Display the value of the output parameter $salesYTD. */ echo "YTD sales for ".$lastName." are ". $salesYTD. "."; +dropProc($conn, 'GetEmployeeSalesYTD', false); + /*Free the statement and connection resources. */ -sqlsrv_free_stmt( $stmt1); -sqlsrv_free_stmt( $stmt2); -sqlsrv_free_stmt( $stmt3); +sqlsrv_free_stmt( $stmt); sqlsrv_close( $conn); ?> --EXPECT-- diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt index f4649d3f..6d86a942 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt @@ -11,34 +11,32 @@ if ( $conn === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_query( $conn, "DROP TABLE dbo.ScrollTest" ); -if ( $stmt !== false ) { - sqlsrv_free_stmt( $stmt ); -} +$tableName = 'ScrollTest'; +dropTable($conn, $tableName); -$stmt = sqlsrv_query( $conn, "CREATE TABLE ScrollTest (id int, value char(10))" ); +$stmt = sqlsrv_query( $conn, "CREATE TABLE $tableName (id int, value char(10))" ); if ( $stmt === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_query( $conn, "INSERT INTO ScrollTest (id, value) VALUES(?,?)", array( 1, "Row 1" )); +$stmt = sqlsrv_query( $conn, "INSERT INTO $tableName (id, value) VALUES(?,?)", array( 1, "Row 1" )); if ( $stmt === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_query( $conn, "INSERT INTO ScrollTest (id, value) VALUES(?,?)", array( 2, "Row 2" )); +$stmt = sqlsrv_query( $conn, "INSERT INTO $tableName (id, value) VALUES(?,?)", array( 2, "Row 2" )); if ( $stmt === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_query( $conn, "INSERT INTO ScrollTest (id, value) VALUES(?,?)", array( 3, "Row 3" )); +$stmt = sqlsrv_query( $conn, "INSERT INTO $tableName (id, value) VALUES(?,?)", array( 3, "Row 3" )); if ( $stmt === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_query( $conn, "SELECT * FROM ScrollTest", array(), array( "Scrollable" => 'keyset' )); -// $stmt = sqlsrv_query( $conn, "SELECT * FROM ScrollTest", array(), array( "Scrollable" => 'dynamic' )); -// $stmt = sqlsrv_query( $conn, "SELECT * FROM ScrollTest", array(), array( "Scrollable" => 'static' )); +$stmt = sqlsrv_query( $conn, "SELECT * FROM $tableName", array(), array( "Scrollable" => 'keyset' )); +// $stmt = sqlsrv_query( $conn, "SELECT * FROM $tableName", array(), array( "Scrollable" => 'dynamic' )); +// $stmt = sqlsrv_query( $conn, "SELECT * FROM $tableName", array(), array( "Scrollable" => 'static' )); $rows = sqlsrv_has_rows( $stmt ); if ( $rows != true ) { @@ -50,9 +48,9 @@ $field1 = sqlsrv_get_field( $stmt, 0 ); $field2 = sqlsrv_get_field( $stmt, 1 ); echo "\n$field1 $field2\n"; -//$stmt2 = sqlsrv_query( $conn, "delete from ScrollTest where id = 3" ); +//$stmt2 = sqlsrv_query( $conn, "delete from $tableName where id = 3" ); // or -$stmt2 = sqlsrv_query( $conn, "UPDATE ScrollTest SET id = 4 WHERE id = 3" ); +$stmt2 = sqlsrv_query( $conn, "UPDATE $tableName SET id = 4 WHERE id = 3" ); if ( $stmt2 !== false ) { sqlsrv_free_stmt( $stmt2 ); } @@ -62,6 +60,8 @@ $field1 = sqlsrv_get_field( $stmt, 0 ); $field2 = sqlsrv_get_field( $stmt, 1 ); echo "\n$field1 $field2\n"; +dropTable($conn, $tableName, false); + sqlsrv_free_stmt( $stmt ); sqlsrv_close( $conn ); ?> diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_server_info.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_server_info.phpt index b941d1e3..00624650 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_server_info.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_server_info.phpt @@ -32,4 +32,4 @@ else sqlsrv_close( $conn); ?> --EXPECTREGEX-- -CurrentDatabase: AdventureWorks.*
SQLServerVersion: 1[2-9].00.[0-9]{4}
SQLServerName: SQL.+
\ No newline at end of file +CurrentDatabase: AdventureWorks201.
SQLServerVersion: 1[2-9].00.[0-9]{4}
SQLServerName: .+
\ No newline at end of file diff --git a/test/bvt/sqlsrv/sqlsrv_param_inout.phpt b/test/bvt/sqlsrv/sqlsrv_param_inout.phpt index 5bc3e2a5..7e97a6d0 100644 --- a/test/bvt/sqlsrv/sqlsrv_param_inout.phpt +++ b/test/bvt/sqlsrv/sqlsrv_param_inout.phpt @@ -15,14 +15,8 @@ call a stored procedure (SQLSRV Driver) and retrieve the errorNumber that is ret } // Drop the stored procedure if it already exists - $tsql_dropSP = "IF OBJECT_ID('sp_Test', 'P') IS NOT NULL - DROP PROCEDURE sp_Test"; - $stmt1 = sqlsrv_query($conn, $tsql_dropSP); - if ($stmt1 === false) { - echo "Error in executing statement 1.\n"; - die (print_r (sqlsrv_errors(), true)); - } - + dropProc($conn, "sp_Test"); + // Create the stored procedure $tsql_createSP = "CREATE PROCEDURE sp_Test @ErrorNumber INT = 0 OUTPUT @@ -31,8 +25,8 @@ call a stored procedure (SQLSRV Driver) and retrieve the errorNumber that is ret SET @ErrorNumber = -1 SELECT 1,2,3 END"; - $stmt2 = sqlsrv_query($conn, $tsql_createSP); - if ($stmt2 === false) { + $stmt = sqlsrv_query($conn, $tsql_createSP); + if ($stmt === false) { echo "Error in executing statement 2.\n"; die (print_r (sqlsrv_errors(), true)); } @@ -47,20 +41,20 @@ call a stored procedure (SQLSRV Driver) and retrieve the errorNumber that is ret ); // Execute the query - $stmt3 = sqlsrv_query($conn, $tsql_callSP, $params); - if ($stmt3 === false) { + $stmt = sqlsrv_query($conn, $tsql_callSP, $params); + if ($stmt === false) { echo "Error in executing statement 3.\n"; die (print_r (sqlsrv_errors(), true)); } // Display the value of the output parameter $errorNumber - sqlsrv_next_result($stmt3); + sqlsrv_next_result($stmt); print("Error Number: $errorNumber\n\n"); - + + dropProc($conn, "sp_Test", false); + // Free the statement and connection resources. */ - sqlsrv_free_stmt( $stmt1); - sqlsrv_free_stmt( $stmt2); - sqlsrv_free_stmt( $stmt3); + sqlsrv_free_stmt( $stmt); sqlsrv_close( $conn); ?> --EXPECT-- diff --git a/test/functional/inc/awc_tee_male_large.gif b/test/functional/inc/awc_tee_male_large.gif new file mode 100644 index 00000000..2191927e Binary files /dev/null and b/test/functional/inc/awc_tee_male_large.gif differ diff --git a/test/functional/inc/silver_chain_large.gif b/test/functional/inc/silver_chain_large.gif new file mode 100644 index 00000000..b65d143b Binary files /dev/null and b/test/functional/inc/silver_chain_large.gif differ diff --git a/test/functional/inc/superlight_black_f_large.gif b/test/functional/inc/superlight_black_f_large.gif new file mode 100644 index 00000000..59b7ca61 Binary files /dev/null and b/test/functional/inc/superlight_black_f_large.gif differ diff --git a/test/functional/inc/test_tvp_data.php b/test/functional/inc/test_tvp_data.php new file mode 100644 index 00000000..f1f31131 --- /dev/null +++ b/test/functional/inc/test_tvp_data.php @@ -0,0 +1,209 @@ + \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index a6edb8d8..44d88454 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -8,6 +8,10 @@ */ +$tvpIncPath = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'inc'.DIRECTORY_SEPARATOR; + +require_once($tvpIncPath. 'test_tvp_data.php'); + // // looks like an additional file (in addition to pdo_test_base.inc) may be needed for these PHPTs // to be runnable from the MSSQL teams' internal proprietary test running system @@ -302,7 +306,7 @@ class BindParamOp } if ($length >= 0 || is_null($length)) { - $this->length = $length; + $this->length = is_null($length) ? 0 : $length; } else { printf("BindParamOp construct: The length provided must be greater or equal to 0.\n"); exit; @@ -527,14 +531,11 @@ function fetchAll($conn, $tbname) * @param string $fetchStyle : fetch_style argument passed to PDOStatement::fetchAll * @return array rows in a result set */ -function selectAll($conn, $tbname, $fetchStyle = null) +function selectAll($conn, $tbname, $fetchStyle = PDO::FETCH_BOTH) { try { $sql = "SELECT * FROM $tbname"; $stmt = $conn->query($sql); - if ($fetchStyle) { - $fetchStyle = constant($fetchStyle); - } $data = $stmt->fetchAll($fetchStyle); return $data; } catch (PDOException $e) { @@ -1794,3 +1795,30 @@ function PhpVersionComponents(&$major, &$minor, &$sub) $minor = strtok("."); $sub = strtok("."); } + +function getTodayDateAsString($conn) +{ + $tsql = 'SELECT CONVERT (VARCHAR(20), GETDATE())'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + return $row[0]; +} + +function compareResourceToInput($actual, $expected) +{ + $size = 8192; + $pos = 0; + $matched = true; + while (!feof($actual)) { + $original = fread($actual, $size); + $str = substr($expected, $pos, $size); + + if ($original !== $str) { + $matched = false; + break; + } + $pos += $size; + } + + return $matched; +} diff --git a/test/functional/pdo_sqlsrv/PDO100_InsertNulls.phpt b/test/functional/pdo_sqlsrv/PDO100_InsertNulls.phpt index 97d712e7..64f9671a 100644 --- a/test/functional/pdo_sqlsrv/PDO100_InsertNulls.phpt +++ b/test/functional/pdo_sqlsrv/PDO100_InsertNulls.phpt @@ -65,7 +65,7 @@ function insertNullsTest($bindType) $stmt2->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_BINARY); $stmt2->bindValue(":p1", null, $bindType); } elseif ($bindType == PDO::PARAM_STR) { - $stmt2->bindParam(":p1", $outvar, $bindType, null, PDO::SQLSRV_ENCODING_BINARY); + $stmt2->bindParam(":p1", $outvar, $bindType, 0, PDO::SQLSRV_ENCODING_BINARY); } } else { $stmt2->bindParam(":p1", $outvar); diff --git a/test/functional/pdo_sqlsrv/PDO27_ReadOnlyAttr.phpt b/test/functional/pdo_sqlsrv/PDO27_ReadOnlyAttr.phpt index dcd61d09..4755808d 100644 --- a/test/functional/pdo_sqlsrv/PDO27_ReadOnlyAttr.phpt +++ b/test/functional/pdo_sqlsrv/PDO27_ReadOnlyAttr.phpt @@ -10,7 +10,7 @@ PHPT_EXEC=true ---EXPECT-- +--EXPECTF-- array(8) { ["flags"]=> int(0) @@ -92,7 +92,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(2) "id" ["len"]=> @@ -110,7 +110,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(3) "val" ["len"]=> @@ -128,7 +128,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(4) "val2" ["len"]=> @@ -146,7 +146,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(0) "" ["len"]=> diff --git a/test/functional/pdo_sqlsrv/PDO51_FetchLOB.phpt b/test/functional/pdo_sqlsrv/PDO51_FetchLOB.phpt index e26f73e7..a271d96c 100644 --- a/test/functional/pdo_sqlsrv/PDO51_FetchLOB.phpt +++ b/test/functional/pdo_sqlsrv/PDO51_FetchLOB.phpt @@ -61,8 +61,14 @@ function fetchLob($offset, $conn, $table, $sqlType, $data1, $data2) if ($id != $data1) { logInfo($offset, "ID data corruption: [$id] instead of [$data1]"); } - if ($label != $data2) { - logInfo($offset, "Label data corruption: [$label] instead of [$data2]"); + if (PHP_VERSION_ID < 80100) { + if ($label != $data2) { + logInfo($offset, "Label data corruption: [$label] instead of [$data2]"); + } + } else { + if (!compareResourceToInput($label, $data2)) { + logInfo($offset, "Label data corruption"); + } } unset($stmt); unset($label); diff --git a/test/functional/pdo_sqlsrv/PDO94_Extend1.phpt b/test/functional/pdo_sqlsrv/PDO94_Extend1.phpt index 3920d93c..cfe413d3 100644 --- a/test/functional/pdo_sqlsrv/PDO94_Extend1.phpt +++ b/test/functional/pdo_sqlsrv/PDO94_Extend1.phpt @@ -5,7 +5,14 @@ Verification of capabilities for extending PDO. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- test1); var_dump($this->test2); $this->test2 = 22; - } + } - function query($sql, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + function query(string $sql, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false { echo __METHOD__ . "()\n"; $stmt = parent::prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement'))); diff --git a/test/functional/pdo_sqlsrv/PDO94_Extend1_p7.phpt b/test/functional/pdo_sqlsrv/PDO94_Extend1_p7.phpt new file mode 100644 index 00000000..0bdafa65 --- /dev/null +++ b/test/functional/pdo_sqlsrv/PDO94_Extend1_p7.phpt @@ -0,0 +1,140 @@ +--TEST-- +Extending PDO Test #1 +--DESCRIPTION-- +Verification of capabilities for extending PDO. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- += 80000) { + die("skip Test designed for PHP 7.*"); +} +?> +--FILE-- +test(); + var_dump($conn1); + + // Prepare test table + DropTable($conn1, $tableName); + $conn1->query("CREATE TABLE [$tableName] (id int NOT NULL PRIMARY KEY, val VARCHAR(10))"); + $conn1->query("INSERT INTO [$tableName] VALUES(0, 'A')"); + $conn1->query("INSERT INTO [$tableName] VALUES(1, 'B')"); + + // Retrieve test data via a direct query + $stmt1 = ExecuteQuery($conn1, "SELECT val, id FROM [$tableName]"); + $result = $stmt1->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_UNIQUE); + var_dump($result); + + + // Cleanup + DropTable($conn1, $tableName); + $stmt1 = null; + $conn1 = null; + + EndTest($testName); +} + +class ExPDO extends PDO +{ + public $test1 = 1; + + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function test() + { + $this->test2 = 2; + var_dump($this->test1); + var_dump($this->test2); + $this->test2 = 22; + } + + function query($sql, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + { + echo __METHOD__ . "()\n"; + $stmt = parent::prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement'))); + $stmt->execute(); + return ($stmt); + } +} + +class ExPDOStatement extends PDOStatement +{ + protected function __construct() + { + echo __METHOD__ . "()\n"; + } + + function __destruct() + { + echo __METHOD__ . "()\n"; + } +} + + + +//-------------------------------------------------------------------- +// Repro +// +//-------------------------------------------------------------------- +function Repro() +{ + + try + { + Extend(); + } + catch (Exception $e) + { + echo $e->getMessage(); + } +} + +Repro(); + +?> +--EXPECTF-- +int(1) +int(2) +object(ExPDO)#%d (2) { + ["test1"]=> + int(1) + ["test2"]=> + int(22) +} +ExPDO::query() +ExPDOStatement::__construct() +ExPDOStatement::__destruct() +ExPDO::query() +ExPDOStatement::__construct() +ExPDOStatement::__destruct() +ExPDO::query() +ExPDOStatement::__construct() +ExPDOStatement::__destruct() +ExPDO::query() +ExPDOStatement::__construct() +array(2) { + ["A"]=> + string(1) "0" + ["B"]=> + string(1) "1" +} +ExPDOStatement::__destruct() +ExPDO::__destruct() +Test "PDO - Extension" completed successfully. \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/PDO95_Extend2.phpt b/test/functional/pdo_sqlsrv/PDO95_Extend2.phpt index f387be22..74530b7d 100644 --- a/test/functional/pdo_sqlsrv/PDO95_Extend2.phpt +++ b/test/functional/pdo_sqlsrv/PDO95_Extend2.phpt @@ -5,7 +5,14 @@ Verification of capabilities for extending PDO. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- protocol(); $args = func_get_args(); return (call_user_func_array(array($this, 'parent::exec'), $args)); } - public function query($statement, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + function query(string $sql, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false { $this->protocol(); $args = func_get_args(); diff --git a/test/functional/pdo_sqlsrv/PDO95_Extend2_p7.phpt b/test/functional/pdo_sqlsrv/PDO95_Extend2_p7.phpt new file mode 100644 index 00000000..694d3a7f --- /dev/null +++ b/test/functional/pdo_sqlsrv/PDO95_Extend2_p7.phpt @@ -0,0 +1,141 @@ +--TEST-- +Extending PDO Test #1 +--DESCRIPTION-- +Verification of capabilities for extending PDO. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- += 80000) { + die("skip Test designed for PHP 7.*"); +} +?> +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + + DropTable($conn2, "tmp_table"); + $conn2->exec("CREATE TABLE tmp_table (id INT)"); + $conn2->exec("INSERT INTO tmp_table (id) VALUES (1), (2)"); + $stmt1 = $conn2->query("SELECT * FROM tmp_table ORDER BY id ASC"); + var_dump($stmt1->fetchAll(PDO::FETCH_ASSOC)); + var_dump($stmt1->fetch()); + $conn2->intercept_call(); + + // Cleanup + DropTable($conn2, "tmp_table"); + $stmt1 = null; + $conn2 = null; + + EndTest($testName); +} + +class ExPDO extends PDO +{ + public function __construct() + { + $this->protocol(); + $args = func_get_args(); + return (call_user_func_array(array($this, 'parent::__construct'), $args)); + } + + public function exec($args1) + { + $this->protocol(); + $args = func_get_args(); + return (call_user_func_array(array($this, 'parent::exec'), $args)); + } + + public function query($statement, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + { + $this->protocol(); + $args = func_get_args(); + return (call_user_func_array(array($this, 'parent::query'), $args)); + } + + public function __call($method, $args) + { + print "__call(".var_export($method, true).", ".var_export($args, true).")\n"; + } + + private function protocol() + { + $stack = debug_backtrace(); + if (!isset($stack[1])) + { + return; + } + printf("%s(", $stack[1]['function']); + $args = ''; + foreach ($stack[1]['args'] as $k => $v) + { + $args .= sprintf("%s, ", var_export($v, true)); + } + if ($args != '') + { + printf("%s", substr($args, 0, -2)); + } + printf(")\n"); + } +} + + +//-------------------------------------------------------------------- +// Repro +// +//-------------------------------------------------------------------- +function Repro() +{ + + try + { + Extend(); + } + catch (Exception $e) + { + echo $e->getMessage(); + } +} + +Repro(); + +?> +--EXPECTF-- +__construct('%s', '%s', '%s') +exec('IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(%s) AND type in (N\'U\')) DROP TABLE %s') +exec('CREATE TABLE %s (id INT)') +exec('INSERT INTO %s (id) VALUES (1), (2)') +query('SELECT * FROM %s ORDER BY id ASC') +array(2) { + [0]=> + array(1) { + ["id"]=> + string(1) "1" + } + [1]=> + array(1) { + ["id"]=> + string(1) "2" + } +} +bool(false) +__call('intercept_call', array ( +)) +exec('IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(%s) AND type in (N\'U\')) DROP TABLE %s') +Test "PDO - Extension" completed successfully. \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/PDO96_Extend3.phpt b/test/functional/pdo_sqlsrv/PDO96_Extend3.phpt index 1a80cc6f..4da5eada 100644 --- a/test/functional/pdo_sqlsrv/PDO96_Extend3.phpt +++ b/test/functional/pdo_sqlsrv/PDO96_Extend3.phpt @@ -5,7 +5,14 @@ Verification of capabilities for extending PDO. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement', array($this)))); diff --git a/test/functional/pdo_sqlsrv/PDO96_Extend3_p7.phpt b/test/functional/pdo_sqlsrv/PDO96_Extend3_p7.phpt new file mode 100644 index 00000000..f6ec2d48 --- /dev/null +++ b/test/functional/pdo_sqlsrv/PDO96_Extend3_p7.phpt @@ -0,0 +1,152 @@ +--TEST-- +Extending PDO Test #2 +--DESCRIPTION-- +Verification of capabilities for extending PDO. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- += 80000) { + die("skip Test designed for PHP 7.*"); +} +?> +--FILE-- +exec("CREATE TABLE [$tableName] (id int NOT NULL PRIMARY KEY, val VARCHAR(10), val2 VARCHAR(16))"); + $stmt1 = $conn1->prepare("INSERT INTO [$tableName] VALUES(?, ?, ?)"); + var_dump(get_class($stmt1)); + foreach ($data as $row) + { + $stmt1->execute($row); + } + unset($stmt1); + + // Retrieve test data via a direct query + $stmt1 = $conn1->query("SELECT * FROM [$tableName]"); + var_dump(get_class($stmt1)); + var_dump(get_class($stmt1->dbh)); + foreach($stmt1 as $obj) + { + var_dump($obj); + } + + echo "===DONE===\n"; + + // Cleanup + DropTable($conn1, $tableName); + $stmt1 = null; + $conn1 = null; + + EndTest($testName); +} + +class ExPDO extends PDO +{ + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function query($sql, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + { + echo __METHOD__ . "()\n"; + $stmt = $this->prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement', array($this)))); + $stmt->setFetchMode(PDO::FETCH_ASSOC); + $stmt->execute(); + return ($stmt); + } +} + +class ExPDOStatement extends PDOStatement +{ + public $dbh; + + protected function __construct($dbh) + { + $this->dbh = $dbh; + echo __METHOD__ . "()\n"; + } + + function __destruct() + { + echo __METHOD__ . "()\n"; + } +} + + +//-------------------------------------------------------------------- +// Repro +// +//-------------------------------------------------------------------- +function Repro() +{ + + try + { + Extend(); + } + catch (Exception $e) + { + echo $e->getMessage(); + } +} + +Repro(); + +?> +--EXPECT-- +string(5) "ExPDO" +string(12) "PDOStatement" +ExPDO::query() +ExPDOStatement::__construct() +string(14) "ExPDOStatement" +string(5) "ExPDO" +array(3) { + ["id"]=> + string(2) "10" + ["val"]=> + string(3) "Abc" + ["val2"]=> + string(3) "zxy" +} +array(3) { + ["id"]=> + string(2) "20" + ["val"]=> + string(3) "Def" + ["val2"]=> + string(3) "wvu" +} +array(3) { + ["id"]=> + string(2) "30" + ["val"]=> + string(3) "Ghi" + ["val2"]=> + string(3) "tsr" +} +===DONE=== +ExPDOStatement::__destruct() +ExPDO::__destruct() +Test "PDO - Extension" completed successfully. \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/PDO97_Extend4.phpt b/test/functional/pdo_sqlsrv/PDO97_Extend4.phpt index 03f31288..646f8121 100644 --- a/test/functional/pdo_sqlsrv/PDO97_Extend4.phpt +++ b/test/functional/pdo_sqlsrv/PDO97_Extend4.phpt @@ -5,7 +5,14 @@ Verification of capabilities for extending PDO. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement', array($this)))); @@ -91,10 +98,10 @@ class ExPDOStatement extends PDOStatement echo __METHOD__ . "()\n"; } - function execute($params = array()) + function execute(?array $params = null) : bool { echo __METHOD__ . "()\n"; - parent::execute(); + return parent::execute(); } } diff --git a/test/functional/pdo_sqlsrv/PDO97_Extend4_p7.phpt b/test/functional/pdo_sqlsrv/PDO97_Extend4_p7.phpt new file mode 100644 index 00000000..37859513 --- /dev/null +++ b/test/functional/pdo_sqlsrv/PDO97_Extend4_p7.phpt @@ -0,0 +1,166 @@ +--TEST-- +Extending PDO Test #3 +--DESCRIPTION-- +Verification of capabilities for extending PDO. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- += 80000) { + die("skip Test designed for PHP 7.*"); +} +?> +--FILE-- +exec("CREATE TABLE [$tableName] (id int NOT NULL PRIMARY KEY, val VARCHAR(10), val2 VARCHAR(16))"); + $stmt1 = $conn1->prepare("INSERT INTO [$tableName] VALUES(?, ?, ?)"); + var_dump(get_class($stmt1)); + foreach ($data as $row) + { + $stmt1->execute($row); + } + unset($stmt1); + + echo "===QUERY===\n"; + + // Retrieve test data via a direct query + $stmt1 = $conn1->query("SELECT * FROM [$tableName]"); + var_dump(get_class($stmt1)); + var_dump(get_class($stmt1->dbh)); + + echo "===FOREACH===\n"; + + foreach($stmt1 as $obj) + { + var_dump($obj); + } + + echo "===DONE===\n"; + + // Cleanup + DropTable($conn1, $tableName); + $stmt1 = null; + $conn1 = null; + + EndTest($testName); +} + +class ExPDO extends PDO +{ + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function query($sql, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + { + echo __METHOD__ . "()\n"; + $stmt = $this->prepare($sql, array(PDO::ATTR_STATEMENT_CLASS=>array('ExPDOStatement', array($this)))); + $stmt->setFetchMode(PDO::FETCH_ASSOC); + $stmt->execute(); + return ($stmt); + } +} + +class ExPDOStatement extends PDOStatement +{ + public $dbh; + + protected function __construct($dbh) + { + $this->dbh = $dbh; + echo __METHOD__ . "()\n"; + } + + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function execute($params = array()) + { + echo __METHOD__ . "()\n"; + parent::execute(); + } +} + + +//-------------------------------------------------------------------- +// Repro +// +//-------------------------------------------------------------------- +function Repro() +{ + + try + { + Extend(); + } + catch (Exception $e) + { + echo $e->getMessage(); + } +} + +Repro(); + +?> +--EXPECT-- +string(5) "ExPDO" +string(12) "PDOStatement" +===QUERY=== +ExPDO::query() +ExPDOStatement::__construct() +ExPDOStatement::execute() +string(14) "ExPDOStatement" +string(5) "ExPDO" +===FOREACH=== +array(3) { + ["id"]=> + string(2) "10" + ["val"]=> + string(3) "Abc" + ["val2"]=> + string(3) "zxy" +} +array(3) { + ["id"]=> + string(2) "20" + ["val"]=> + string(3) "Def" + ["val2"]=> + string(3) "wvu" +} +array(3) { + ["id"]=> + string(2) "30" + ["val"]=> + string(3) "Ghi" + ["val2"]=> + string(3) "tsr" +} +===DONE=== +ExPDOStatement::__destruct() +ExPDO::__destruct() +Test "PDO - Extension" completed successfully. \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/PDO98_Extend5.phpt b/test/functional/pdo_sqlsrv/PDO98_Extend5.phpt index a3e9c94b..e28c7f01 100644 --- a/test/functional/pdo_sqlsrv/PDO98_Extend5.phpt +++ b/test/functional/pdo_sqlsrv/PDO98_Extend5.phpt @@ -5,7 +5,14 @@ Verification of capabilities for extending PDO. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- = 80000) { + die("skip Test designed for PHP 7.*"); +} +?> +--FILE-- +exec("CREATE TABLE [$tableName] (id int NOT NULL PRIMARY KEY, val VARCHAR(10), val2 VARCHAR(16))"); + $stmt1 = $conn1->prepare("INSERT INTO [$tableName] VALUES(?, ?, ?)"); + var_dump(get_class($stmt1)); + foreach ($data as $row) + { + $stmt1->execute($row); + } + unset($stmt1); + + echo "===QUERY===\n"; + + // Retrieve test data via a direct query + var_dump($conn1->getAttribute(PDO::ATTR_STATEMENT_CLASS)); + $conn1->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('ExPDOStatement', array($conn1))); + var_dump($conn1->getAttribute(PDO::ATTR_STATEMENT_CLASS)); + $stmt1 = $conn1->query("SELECT * FROM [$tableName]"); + var_dump(get_class($stmt1)); + var_dump(get_class($stmt1->dbh)); + + echo "===FOREACH===\n"; + + foreach($stmt1 as $obj) + { + var_dump($obj); + } + + echo "===DONE===\n"; + + // Cleanup + DropTable($conn1, $tableName); + $stmt1 = null; + $conn1 = null; + + EndTest($testName); +} + +class ExPDO extends PDO +{ + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function query($sql, $fetch_style = PDO::FETCH_BOTH,...$fetch_mode_args) + { + echo __METHOD__ . "()\n"; + $stmt = parent::query($sql); + return ($stmt); + } +} + +class ExPDOStatement extends PDOStatement +{ + public $dbh; + + protected function __construct($dbh) + { + $this->dbh = $dbh; + $this->setFetchMode(PDO::FETCH_ASSOC); + echo __METHOD__ . "()\n"; + } + + function __destruct() + { + echo __METHOD__ . "()\n"; + } + + function execute($params = array()) + { + echo __METHOD__ . "()\n"; + parent::execute(); + } +} + + +//-------------------------------------------------------------------- +// Repro +// +//-------------------------------------------------------------------- +function Repro() +{ + + try + { + Extend(); + } + catch (Exception $e) + { + echo $e->getMessage(); + } +} + +Repro(); + +?> +--EXPECTF-- +string(5) "ExPDO" +string(12) "PDOStatement" +===QUERY=== +array(1) { + [0]=> + string(12) "PDOStatement" +} +array(2) { + [0]=> + string(14) "ExPDOStatement" + [1]=> + array(1) { + [0]=> + object(ExPDO)#%d (0) { + } + } +} +ExPDO::query() +ExPDOStatement::__construct() +string(14) "ExPDOStatement" +string(5) "ExPDO" +===FOREACH=== +array(3) { + ["id"]=> + string(2) "10" + ["val"]=> + string(3) "Abc" + ["val2"]=> + string(3) "zxy" +} +array(3) { + ["id"]=> + string(2) "20" + ["val"]=> + string(3) "Def" + ["val2"]=> + string(3) "wvu" +} +array(3) { + ["id"]=> + string(2) "30" + ["val"]=> + string(3) "Ghi" + ["val2"]=> + string(3) "tsr" +} +===DONE=== +ExPDOStatement::__destruct() +Test "PDO - Extension" completed successfully. +ExPDO::__destruct() \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_022_xml_bind_value.phpt b/test/functional/pdo_sqlsrv/pdo_022_xml_bind_value.phpt index 64ffa50b..5e1a34b5 100644 --- a/test/functional/pdo_sqlsrv/pdo_022_xml_bind_value.phpt +++ b/test/functional/pdo_sqlsrv/pdo_022_xml_bind_value.phpt @@ -42,7 +42,7 @@ try { } // Get data -$row = selectAll($conn, $tableName, "PDO::FETCH_ASSOC"); +$row = selectAll($conn, $tableName, PDO::FETCH_ASSOC); var_dump($row); // Close connection diff --git a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name.phpt b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name.phpt index 2a8236b3..0c7d89fc 100644 --- a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name.phpt +++ b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name.phpt @@ -28,9 +28,18 @@ try { $stmt = $conn->prepare("SELECT Value FROM $tableName"); $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); $stmt->execute(); - $stmt->fetch(PDO::FETCH_BOUND); - var_dump($val1 === $value); - + $stmt->fetch(PDO::FETCH_BOUND); + + if (PHP_VERSION_ID < 80100) { + var_dump($val1 === $value); + } else { + // $val1 is a stream object + if (!feof($val1)) { + $str = fread($val1, 8192); + var_dump($str === $value); + } + } + // Close connection dropTable($conn, $tableName); unset($stmt); diff --git a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt index 39d30729..98d68f63 100644 --- a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt +++ b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt @@ -148,7 +148,16 @@ try { $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); $stmt->execute(); $stmt->fetch(PDO::FETCH_BOUND); - var_dump($val1 === $value); + + if (PHP_VERSION_ID < 80100) { + var_dump($val1 === $value); + } else { + // $val1 is a stream object + if (!feof($val1)) { + $str = fread($val1, 8192); + var_dump($str === $value); + } + } // Close connection dropTable($conn, $tableName); diff --git a/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt index 5b39f9f1..478bcf91 100644 --- a/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt +++ b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt @@ -57,7 +57,12 @@ try { // Start testing quote function $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_CHAR); - var_dump($conn->quote(null, PDO::PARAM_NULL)); + // Deprecated: PDO::quote(): Passing null to parameter #1 ($string) of type string is being deprecated + if (PHP_VERSION_ID < 80100) { + var_dump($conn->quote(null, PDO::PARAM_NULL)); + } else { + var_dump($conn->quote('', PDO::PARAM_NULL)); + } var_dump($conn->quote('\'', PDO::PARAM_STR)); var_dump($conn->quote('foo', PDO::PARAM_STR)); var_dump($conn->quote('foo', PDO::PARAM_STR | PDO::PARAM_STR_CHAR)); diff --git a/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php b/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php index f77cabff..4140f80a 100644 --- a/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php +++ b/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php @@ -25,6 +25,22 @@ function printMoney($amt, $info) echo PHP_EOL; } +function printCal($date) +{ + $loc = setlocale(LC_TIME, 0); + $fmt = datefmt_create( + $loc, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + 'America/Los_Angeles', + IntlDateFormatter::GREGORIAN + ); + datefmt_set_pattern($fmt, 'cccc'); + echo datefmt_format($fmt, $date) . PHP_EOL; + datefmt_set_pattern($fmt, 'MMMM'); + echo datefmt_format($fmt, $date) . PHP_EOL; +} + require_once('MsSetup.inc'); $setLocaleInfo = ($_SERVER['argv'][1]); @@ -90,8 +106,8 @@ if ($sep !== $info['thousands_sep']) { $n1 = 10000.98765; printMoney($n1, $info); -echo strftime("%A", strtotime("12/25/2020")) . PHP_EOL; -echo strftime("%B", strtotime("12/25/2020")) . PHP_EOL; +$d = new DateTime("12/25/2020", new DateTimeZone('America/Los_Angeles')); +printCal($d); try { $conn = new PDO("sqlsrv:server = $server; database=$databaseName; driver=$driver", $uid, $pwd ); diff --git a/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt b/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt index 1ba60606..0bbfcff3 100644 --- a/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt +++ b/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt @@ -38,7 +38,7 @@ try { } ?> ---EXPECT-- +--EXPECTF-- array(8) { ["flags"]=> int(0) @@ -49,7 +49,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(6) "RESULT" ["len"]=> diff --git a/test/functional/pdo_sqlsrv/pdo_1258_is_callable_error.phpt b/test/functional/pdo_sqlsrv/pdo_1258_is_callable_error.phpt new file mode 100644 index 00000000..0f0f9198 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1258_is_callable_error.phpt @@ -0,0 +1,50 @@ +--TEST-- +GitHub issue 1258 - is_callable() throws an exception if PDOStatement method does not exist +--DESCRIPTION-- +The test shows is_callable() will return false if PDOStatement method does not exist instead of throwing an exception. The user can still check errorInfo() for the error message. See documentation https://www.php.net/manual/en/function.is-callable.php +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $stmt = $conn->prepare("SELECT @@Version"); + $functionExists = is_callable([$stmt, 'bindParam'], false, $callable); + var_dump($functionExists); + var_dump($callable); + + $functionExists = is_callable([$stmt, 'boo']); + var_dump($functionExists); + + echo PHP_EOL . "Error INFO:" . PHP_EOL; + var_dump($conn->errorInfo()); + + echo "Done\n"; +} catch (PdoException $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +bool(true) +string(23) "PDOStatement::bindParam" +bool(false) + +Error INFO: +array(3) { + [0]=> + string(5) "IMSSP" + [1]=> + int(-58) + [2]=> + string(48) "This function is not implemented by this driver." +} +Done + diff --git a/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt b/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt new file mode 100644 index 00000000..4ab0afe5 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1261_test_ascii_utf8.phpt @@ -0,0 +1,57 @@ +--TEST-- +Verify Github Issue 1261 is fixed. +--DESCRIPTION-- +This test should already pass in Windows so it is mainly aimed for non-Windows settings where UTF-8 is the default encoding. ODBC warnings are handled differently with pdo_sqlsrv so logging is used to checking the warnings. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_SYSTEM); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + # leading string >= 2045 leading to result length > 2048 + # column must be VARCHAR(MAX) or VARCHAR(2048) (starts working with bigger VARCHAR(n), e.g. 2060) + # SQLSRV_ATTR_ENCODING must be set to SQLSRV_ENCODING_SYSTEM (works with PDO::SQLSRV_ENCODING_UTF8) + # COLLATE must not be %UTF8% (e.g. Latin1_General_100_CI_AS_SC_UTF8 works) + + $sql = "SET NOCOUNT ON; + DECLARE @val VARCHAR(8000) = REPLICATE('a', 2045) + 'ñ'; + CREATE TABLE #tmpTest (testCol VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS); + INSERT INTO #tmpTest (testCol) VALUES (@val); + SELECT * from #tmpTest;"; + + $stmt = $conn->query($sql); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + var_dump($row); + + echo file_get_contents($logFilepath); + unlink($logFilepath); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e); +} +?> +--EXPECTF-- +array(1) { + ["testCol"]=> + string(2049) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaañ" +} +[%s] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000 +[%s] pdo_sqlsrv_db_handle_factory: error code = 5701 +[%s] pdo_sqlsrv_db_handle_factory: message = [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Changed database context to '%s'. +[%s] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000 +[%s] pdo_sqlsrv_db_handle_factory: error code = 5703 +[%s] pdo_sqlsrv_db_handle_factory: message = [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Changed language setting to %s. + diff --git a/test/functional/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt b/test/functional/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt index c089e694..a99fbddf 100644 --- a/test/functional/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt +++ b/test/functional/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt @@ -10,8 +10,8 @@ try { $cnn = connect(); $pdo_options = array(); - if (!isColEncrypted()) { - // Emulate prepare and direct query are not supported with Always Encrypted + if (!isAEConnected()) { + // Emulate prepare and direct query not supported when connected with Always Encrypted $pdo_options[PDO::ATTR_EMULATE_PREPARES] = TRUE; $pdo_options[PDO::SQLSRV_ATTR_DIRECT_QUERY] = TRUE; } @@ -21,17 +21,25 @@ try { $tbname = "watchdog"; createTable( $cnn, $tbname, array( "system_encoding" => "nvarchar(128)", "utf8_encoding" => "nvarchar(128)", "binary_encoding" => "varbinary(max)")); + $query = <<prepare($query, $pdo_options); + $system_param = 'system encoded string'; $utf8_param = '가각ácasa'; $binary_param = fopen('php://memory', 'a'); fwrite($binary_param, 'asdgasdgasdgsadg'); rewind($binary_param); - $inputs = array("system_encoding" => $system_param, - "utf8_encoding" => new BindParamOp( 2, $utf8_param, "PDO::PARAM_STR", 0, "PDO::SQLSRV_ENCODING_UTF8" ), - "binary_encoding" => new BindParamOp( 3, $binary_param, "PDO::PARAM_LOB", 0, "PDO::SQLSRV_ENCODING_BINARY" )); + $st->bindParam(1, $system_param, PDO::PARAM_STR); + $st->bindParam(2, $utf8_param, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8); + $st->bindParam(3, $binary_param, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); - insertRow($cnn, $tbname, $inputs, "prepareBindParam"); + $st->execute(); $data = selectAll($cnn, $tbname); var_dump($data); diff --git a/test/functional/pdo_sqlsrv/pdo_270_fetch_binary.phpt b/test/functional/pdo_sqlsrv/pdo_270_fetch_binary.phpt index f3f52b80..7ce39da6 100644 --- a/test/functional/pdo_sqlsrv/pdo_270_fetch_binary.phpt +++ b/test/functional/pdo_sqlsrv/pdo_270_fetch_binary.phpt @@ -50,7 +50,23 @@ try { var_dump($e->errorInfo); } - //calls various fetch methods +function verifyBinaryResult($result, $input, $len, $message) +{ + if (PHP_VERSION_ID < 80100) { + if (strncmp($result, $input, $len) !== 0) { + print_r($message); + } + } else { + if (!feof($result)) { + $str = fread($result, $len); + if (strncmp($str, $input, $len) !== 0) { + print_r($message); + } + } + } +} + +//calls various fetch methods function testFetch($conn, $tableName, $columnName, $input) { $len = strlen($input); @@ -60,17 +76,13 @@ function testFetch($conn, $tableName, $columnName, $input) $stmt->bindColumn(1, $result, PDO::PARAM_LOB); $stmt->fetch(PDO::FETCH_BOUND); //binary is fixed size, to evaluate output, compare it using strncmp - if (strncmp($result, $input, $len) !== 0) { - print_r("\nRetrieving using bindColumn failed"); - } + verifyBinaryResult($result, $input, $len, "\nRetrieving using bindColumn failed"); $result = ""; $stmt = $conn->query($sql); $stmt->bindColumn(1, $result, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); $stmt->fetch(PDO::FETCH_BOUND); - if (strncmp($result, $input, $len) !== 0) { - print_r("\nRetrieving using bindColumn with encoding set failed"); - } + verifyBinaryResult($result, $input, $len, "\nRetrieving using bindColumn with encoding set failed"); $result = ""; $stmt = $conn->query($sql); diff --git a/test/functional/pdo_sqlsrv/pdo_538_quote_nul.phpt b/test/functional/pdo_sqlsrv/pdo_538_quote_nul.phpt index dc80e35b..d1a7ea7e 100644 --- a/test/functional/pdo_sqlsrv/pdo_538_quote_nul.phpt +++ b/test/functional/pdo_sqlsrv/pdo_538_quote_nul.phpt @@ -15,6 +15,12 @@ try { print("Original: " . str_replace("\0", "{NUL}", $str) . "\n"); $str = $connection->quote($str); print("Quoted: " . str_replace("\0", "{NUL}", $str) . "\n"); + + $str1 = "X'X\0'XX"; + print("Original: " . str_replace("\0", "{NUL}", $str1) . "\n"); + $str1 = $connection->quote($str1); + print("Quoted: " . str_replace("\0", "{NUL}", $str1) . "\n"); + } catch (PDOException $e) { die("Connection error: " . $e->getMessage()); } @@ -22,3 +28,5 @@ try { --EXPECT-- Original: XX{NUL}XX Quoted: 'XX{NUL}XX' +Original: X'X{NUL}'XX +Quoted: 'X''X{NUL}''XX' diff --git a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt index d41175a2..83395bd5 100644 --- a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt +++ b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt @@ -45,9 +45,23 @@ try { $stmt->bindColumn(2, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_SYSTEM); $result = $stmt->fetch(PDO::FETCH_BOUND); - if (!$result || $value !== $input[1]) { - echo "Expected $input[1] but got: "; - var_dump($result); + if (PHP_VERSION_ID < 80100) { + if (!$result || $value !== $input[1]) { + echo "Expected $input[1] but got: "; + var_dump($value); + } + } else { + if (!$result || !is_resource($value)) { + echo "Expected a stream resource but got: "; + var_dump($value); + } + if (!feof($value)) { + $str = fread($value, strlen($input[1])); + if ($str !== $input[1]) { + echo "Expected $input[1] but got: "; + var_dump($str); + } + } } $stmt->bindColumn(2, $value, PDO::PARAM_STR); diff --git a/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt b/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt index d0cad7e7..403e5cb7 100644 --- a/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt +++ b/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt @@ -79,7 +79,7 @@ unset($stmt); unset($conn); ?> ---EXPECT-- +--EXPECTF-- Number of columns after UPDATE: 0 array(8) { ["flags"]=> @@ -91,7 +91,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(2) "id" ["len"]=> @@ -109,7 +109,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(4) "name" ["len"]=> diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_binary_size.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_binary_size.phpt index 253460ea..2f320ce6 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_binary_size.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_binary_size.phpt @@ -52,9 +52,15 @@ try { if (!is_null($det) || !is_null($rand)) { echo "Retrieving $typeFull data as $pdoParamType should not be supported\n"; } - // check the case when fetching as PDO::PARAM_STR or PDO::PARAM_LOB + // check the case when fetching as PDO::PARAM_STR // with or without AE: should work } else { + if (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_LOB") { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } + if (trim($det) == $inputValues[0] && trim($rand) == $inputValues[1]) { echo "****Retrieving $typeFull data as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_char_size.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_char_size.phpt index e575d3fd..4fffc614 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_char_size.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_char_size.phpt @@ -53,6 +53,10 @@ try { // check the case when fetching as PDO::PARAM_STR or PDO::PARAM_LOB // with or without AE: should work } else { + if (PHP_VERSION_ID >= 80100 && is_resource($c1)) { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $c1 = fread($c1, 8192); + } if (strlen($c1) == $m) { echo "****Retrieving $typeFull as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime.phpt index b1cf8235..7fce47d4 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime.phpt @@ -47,6 +47,11 @@ try { // only check if input values are part of fetched values because some input values do not contain any deicmal places, the value retrieved however has 3 decimal places if the type is a datetime // with or without AE: should work } else { + if (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_LOB") { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } if (strpos($det, $inputValues[0]) !== false && strpos($rand, $inputValues[1]) !== false) { echo "****Retrieving $dataType as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime_precision.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime_precision.phpt index cb92c950..0d2947f1 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime_precision.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_datetime_precision.phpt @@ -83,6 +83,11 @@ try { // check the case when fetching as PDO::PARAM_STR or PDO::PARAM_LOB // with or without AE: should work } else { + if (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_LOB") { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } if (compareDate($det, $inputValues[0], $dataType) && compareDate($rand, $inputValues[1], $dataType)) { echo "****Retrieving $typeFull as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_decimal_precision.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_decimal_precision.phpt index ce4aa395..e9d882c2 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_decimal_precision.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_decimal_precision.phpt @@ -114,6 +114,11 @@ try { $succeeded = compareIntegers($pdoParamType, $det, $rand, $inputValues, $m1, $m2); } } else { + if (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_LOB") { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } if (abs($det - $inputValues[0]) < $epsilon && abs($rand - $inputValues[1]) < $epsilon) { $succeeded = true; diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_float_bits.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_float_bits.phpt index 8eb2b918..b234a725 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_float_bits.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_float_bits.phpt @@ -57,6 +57,12 @@ try { echo "Retriving $typeFull data as $pdoParamType should return NULL\n"; } } else { + if (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_LOB") { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } + if (abs($det - $inputValues[0]) < $epsilon && abs($rand - $inputValues[1]) < $epsilon) { echo "****Retrieving $typeFull as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_nchar_size.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_nchar_size.phpt index 43ead937..bb374008 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_nchar_size.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_nchar_size.phpt @@ -53,6 +53,10 @@ try { // check the case when fetching as PDO::PARAM_STR or PDO::PARAM_LOB // with or without AE: should work } else { + if (PHP_VERSION_ID >= 80100 && is_resource($c1)) { + // Starting with PHP 8.1 fetching as PDO::PARAM_LOB will return a resource obj + $c1 = fread($c1, 8192); + } if (strlen($c1) == $m) { echo "****Retrieving $typeFull as $pdoParamType is supported****\n"; } else { diff --git a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_numeric.phpt b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_numeric.phpt index 1a281bdb..ff17dc7e 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_numeric.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_bindColumn_pdoparam_numeric.phpt @@ -48,27 +48,41 @@ try { } // check the case when fetching as PDO::PARAM_BOOL or PDO::PARAM_INT // with or without AE: should only not work with bigint - } else if ($pdoParamType == "PDO::PARAM_BOOL" || $pdoParamType == "PDO::PARAM_INT") { + } elseif ($pdoParamType == "PDO::PARAM_BOOL" || $pdoParamType == "PDO::PARAM_INT") { if ($dataType == "bigint") { if (!is_null($det) || !is_null($rand)) { echo "Retrieving $dataType data as $pdoParamType should not be supported\n"; } - } else if ($dataType == "real") { - if (abs($det - $inputValues[0]) < $epsilon && abs($rand - $inputValues[1]) < $epsilon) { + } elseif (PHP_VERSION_ID >= 80100 && $pdoParamType == "PDO::PARAM_BOOL") { + if ($det == boolval($inputValues[0]) && $rand == boolval($inputValues[1])) { echo "****Retrieving $dataType as $pdoParamType is supported****\n"; } else { echo "Retrieving $dataType as $pdoParamType fails\n"; } } else { - if ($det == $inputValues[0] && $rand == $inputValues[1]) { - echo "****Retrieving $dataType as $pdoParamType is supported****\n"; + if ($dataType == "real") { + if (abs($det - $inputValues[0]) < $epsilon && abs($rand - $inputValues[1]) < $epsilon) { + echo "****Retrieving $dataType as $pdoParamType is supported****\n"; + } else { + echo "Retrieving $dataType as $pdoParamType fails\n"; + } } else { - echo "Retrieving $dataType as $pdoParamType fails\n"; + if ($det == $inputValues[0] && $rand == $inputValues[1]) { + echo "****Retrieving $dataType as $pdoParamType is supported****\n"; + } else { + echo "Retrieving $dataType as $pdoParamType fails\n"; + } } } // check the case when fetching as PDO::PARAM_BOOL, PDO::PARAM_INT, PDO::PARAM_STR or PDO::PARAM_LOB // with or without AE: should work } else { + if ($pdoParamType == "PDO::PARAM_LOB") { + if (PHP_VERSION_ID >= 80100) { + $det = fread($det, 8192); + $rand = fread($rand, 8192); + } + } if ($dataType == "real") { if (abs($det - $inputValues[0]) < $epsilon && abs($rand - $inputValues[1]) < $epsilon) { echo "****Retrieving $dataType as $pdoParamType is supported****\n"; diff --git a/test/functional/pdo_sqlsrv/pdo_ae_output_param_floats.phpt b/test/functional/pdo_sqlsrv/pdo_ae_output_param_floats.phpt index 3fccd103..a12a6c4b 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_output_param_floats.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_output_param_floats.phpt @@ -1,7 +1,8 @@ --TEST-- Test for retrieving encrypted data of floats as output parameters --DESCRIPTION-- -Use PDOstatement::bindParam with all PDO::PARAM_ types +Use PDOstatement::bindParam with all PDO::PARAM_ types. This test generates input float. For your reference: +https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limitations_on_integer_values --ENV-- PHPT_EXEC=true --SKIPIF-- @@ -51,9 +52,9 @@ function testOutputFloats($fetchNumeric, $inout) $inputValues = array(); // create random input values for ($i = 0; $i < 2; $i++) { - $mantissa = rand(1, 100000000); - $decimals = rand(1, 100000000); - $floatNum = $mantissa + $decimals / 10000000; + $mantissa = rand(1, 10000000); + $decimals = rand(1, 100); + $floatNum = $mantissa + $decimals / 10000; if ($i > 0) { // make the second input negative $floatNum *= -1; diff --git a/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt index ebf19765..9204f684 100644 --- a/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt +++ b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt @@ -129,8 +129,14 @@ function fetchBinaryAsBinary($conn, $tableName, $inputs) $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"; + if (PHP_VERSION_ID < 80100) { + if ($binaryValue !== $inputs[0]) { + echo "Fetched binary value unexpected: $binaryValue\n"; + } + } else { + if (!compareResourceToInput($binaryValue, $inputs[0])) { + echo "Fetched binary value unexpected\n"; + } } } catch (PdoException $e) { echo "Caught exception in fetchBinaryAsBinary:\n"; diff --git a/test/functional/pdo_sqlsrv/pdo_connect_driver.phpt b/test/functional/pdo_sqlsrv/pdo_connect_driver.phpt index 41431fa5..a1a18144 100644 --- a/test/functional/pdo_sqlsrv/pdo_connect_driver.phpt +++ b/test/functional/pdo_sqlsrv/pdo_connect_driver.phpt @@ -109,7 +109,7 @@ function testEncryptedWithODBC() $value = "ODBC Driver 13 for SQL Server"; $connectionOptions = "Driver = $value; ColumnEncryption = Enabled;"; - $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; + $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server"; connectVerifyOutput($connectionOptions, $expected); } diff --git a/test/functional/pdo_sqlsrv/pdo_data_classification.phpt b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt index 13add5bb..ce734a9c 100644 --- a/test/functional/pdo_sqlsrv/pdo_data_classification.phpt +++ b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt @@ -50,7 +50,8 @@ function testConnAttrCases() $conn = new PDO($dsn, $uid, $pwd, $attr); $conn->getAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION); } catch (PDOException $e) { - if (!fnmatch($noSupportErr, $e->getMessage())) { + $expected = (PHP_VERSION_ID < 80100) ? $noSupportErr : $stmtErr; + if (!fnmatch($expected, $e->getMessage())) { echo "Connection attribute test (3) unexpected\n"; var_dump($e->getMessage()); } diff --git a/test/functional/pdo_sqlsrv/pdo_data_classification_ranks.phpt b/test/functional/pdo_sqlsrv/pdo_data_classification_ranks.phpt index f97be99e..df57f6e6 100644 --- a/test/functional/pdo_sqlsrv/pdo_data_classification_ranks.phpt +++ b/test/functional/pdo_sqlsrv/pdo_data_classification_ranks.phpt @@ -52,7 +52,8 @@ function testConnAttrCases() $conn = new PDO($dsn, $uid, $pwd, $attr); $conn->getAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION); } catch (PDOException $e) { - if (!fnmatch($noSupportErr, $e->getMessage())) { + $expected = (PHP_VERSION_ID < 80100) ? $noSupportErr : $stmtErr; + if (!fnmatch($expected, $e->getMessage())) { echo "Connection attribute test (3) unexpected\n"; var_dump($e->getMessage()); } diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_cursorBuffered_float_bindColumn_lob.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_cursorBuffered_float_bindColumn_lob.phpt index 4239aef5..8682a6bc 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_cursorBuffered_float_bindColumn_lob.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_cursorBuffered_float_bindColumn_lob.phpt @@ -30,6 +30,9 @@ try { $stmt->execute(); $stmt->bindColumn('exist', $float_col, PDO::PARAM_LOB); $value = $stmt->fetch(); + if (PHP_VERSION_ID >= 80100) { + $float_col = fread($float_col, 8192); + } var_dump($float_col); print "\nno buffered cursor, stringify off, fetch_numeric on\n"; @@ -38,6 +41,9 @@ try { $stmt->execute(); $stmt->bindColumn('exist', $float_col, PDO::PARAM_LOB); $value = $stmt->fetch(); + if (PHP_VERSION_ID >= 80100) { + $float_col = fread($float_col, 8192); + } var_dump($float_col); print "\nno buffered cursor, stringify on, fetch_numeric on\n"; @@ -63,6 +69,9 @@ try { $stmt->execute(); $stmt->bindColumn('exist', $float_col, PDO::PARAM_LOB); $value = $stmt->fetch(); + if (PHP_VERSION_ID >= 80100) { + $float_col = fread($float_col, 8192); + } var_dump($float_col); print "\nbuffered cursor, stringify off, fetch_numeric on\n"; @@ -71,6 +80,9 @@ try { $stmt->execute(); $stmt->bindColumn('exist', $float_col, PDO::PARAM_LOB); $value = $stmt->fetch(); + if (PHP_VERSION_ID >= 80100) { + $float_col = fread($float_col, 8192); + } var_dump($float_col); print "\nbuffered cursor, stringify on, fetch_numeric on\n"; diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt index 8463875b..b2747edf 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt @@ -135,7 +135,12 @@ function runTest($conn, $query, $columns, $values, $useBuffer = false) // Setting it to true only converts numeric values to strings when fetching // See http://www.php.net/manual/en/pdo.setattribute.php for details // stringify on, fetch_numeric off, fetch_datetime on - $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + if (PHP_VERSION_ID < 80100) { + // TODO: starting in PHP 8.1 with ATTR_STRINGIFY_FETCHES set to true + // this fails with this error from PHP: + // Fatal error: Uncaught Error: Object of class DateTime could not be converted to string + $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + } $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); $stmt = $conn->prepare($query, $options); diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_large_stream.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_large_stream.phpt index 5f106b2e..b3d529ba 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_large_stream.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_large_stream.phpt @@ -23,6 +23,9 @@ $nstrValue = str_repeat("ÃÜðßZZýA©", 200); function checkData($actual, $expected) { + if (PHP_VERSION_ID >= 80100 && is_resource($actual)) { + $actual = fread($actual, 8192); + } trace("Actual:\n$actual\n"); $success = true; diff --git a/test/functional/pdo_sqlsrv/pdo_getAttribute_clientInfo.phpt b/test/functional/pdo_sqlsrv/pdo_getAttribute_clientInfo.phpt index 6445fb94..521940ba 100644 --- a/test/functional/pdo_sqlsrv/pdo_getAttribute_clientInfo.phpt +++ b/test/functional/pdo_sqlsrv/pdo_getAttribute_clientInfo.phpt @@ -23,5 +23,5 @@ Array \[(DriverDllName|DriverName)\] => (msodbcsql1[1-9].dll|(libmsodbcsql-[0-9]{2}\.[0-9]\.so\.[0-9]\.[0-9]|libmsodbcsql.[0-9]{2}.dylib)) \[DriverODBCVer\] => [0-9]{1,2}\.[0-9]{1,2} \[DriverVer\] => [0-9]{1,2}\.[0-9]{1,2}\.[0-9]{4} - \[ExtensionVer\] => [0-9]\.[0-9]\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)? + \[ExtensionVer\] => [0-9]\.[0-9]+\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)? \) diff --git a/test/functional/pdo_sqlsrv/pdo_get_attr_invalid.phpt b/test/functional/pdo_sqlsrv/pdo_get_attr_invalid.phpt index 09be0c7d..5ab1f50d 100644 --- a/test/functional/pdo_sqlsrv/pdo_get_attr_invalid.phpt +++ b/test/functional/pdo_sqlsrv/pdo_get_attr_invalid.phpt @@ -7,12 +7,33 @@ Test getting invalid attributes require_once("MsCommon_mid-refactor.inc"); +// When testing with PHP 8.1-dev, pdo_sqlsrv handles unsupported attribute differently. +// Implement a custom warning handler such that this test works with previous PHP versions as well. +function warningHandler($errno, $errstr) +{ + $warning = "Driver does not support this function: driver does not support that attribute"; + $str = strstr($errstr, $warning); + if ($str == false) { + echo "Unexpected warning message:"; + var_dump($errstr); + } +} + try { $conn = connect("", array(), PDO::ERRMODE_SILENT); + set_error_handler("warningHandler", E_WARNING); @$conn->getAttribute(PDO::ATTR_FETCH_TABLE_NAMES); - print_r(($conn->errorInfo())[2]); - echo "\n"; + + // Starting with PHP 8.1-dev getting an unsupported attribute pdo_sqlsrv will no longer + // throw an exception. PHP PDO will handle the warning instead. + if (PHP_VERSION_ID < 80100) { + $errmsg = ($conn->errorInfo())[2]; + if ($errmsg !== "An unsupported attribute was designated on the PDO object.") { + var_dump($conn->errorInfo()); + } + } + restore_error_handler(); @$conn->getAttribute(PDO::ATTR_CURSOR); print_r(($conn->errorInfo())[2]); @@ -26,6 +47,5 @@ try { } ?> --EXPECT-- -An unsupported attribute was designated on the PDO object. The given attribute is only supported on the PDOStatement object. An invalid attribute was designated on the PDO object. diff --git a/test/functional/pdo_sqlsrv/pdo_get_set_attr.phpt b/test/functional/pdo_sqlsrv/pdo_get_set_attr.phpt index e2a9610f..37178afe 100644 --- a/test/functional/pdo_sqlsrv/pdo_get_set_attr.phpt +++ b/test/functional/pdo_sqlsrv/pdo_get_set_attr.phpt @@ -131,7 +131,7 @@ array\(4\) { \["DriverVer"\]=> string\(10\) "[0-9]{2}.[0-9]{2}.[0-9]{4}" \["ExtensionVer"\]=> - string\([0-9]*\) \"[0-9].[0-9]\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)?\" + string\([0-9]*\) \"[0-9].[0-9]+\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)?\" } Test_6: diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt index c90e3135..387fbb42 100644 --- a/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt @@ -26,7 +26,7 @@ try { $insertSql = "INSERT INTO $tableName (c1) VALUES (?)"; $stmt = $conn->prepare($insertSql); - $stmt->bindParam(1, $invalidUTF16, PDO::PARAM_STR, null, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(1, $invalidUTF16, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_BINARY); $stmt->execute(); try { diff --git a/test/functional/pdo_sqlsrv/pdo_lastInsertId.phpt b/test/functional/pdo_sqlsrv/pdo_lastInsertId.phpt index 7dfe4bbb..99ea03a5 100644 --- a/test/functional/pdo_sqlsrv/pdo_lastInsertId.phpt +++ b/test/functional/pdo_sqlsrv/pdo_lastInsertId.phpt @@ -40,7 +40,7 @@ try { ?> ---EXPECT-- -string(3) "200" -string(3) "102" -string(0) "" +--EXPECTREGEX-- +string\(3\) "200" +string\(3\) "102" +(string\(0\) ""|bool\(false\)) diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode.phpt index 025f94c3..71a35f82 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode.phpt @@ -6,7 +6,7 @@ prepare with emulate prepare and binding uft8 characters +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + dropProc($conn, 'SelectTVP2'); + + $tvpType = 'TestTVP2'; + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create table type and a stored procedure + $conn->exec($createTestTVP2); + $conn->exec($createSelectTVP2); + + // Create column arrays + $str1 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; + $longStr1 = str_repeat($str1, 1500); + $str2 = pack("H*", '49006427500048005000' ); // I'LOVE_SYMBOL'PHP + $longStr2 = str_repeat($str2, 2000); + + $bin1 = pack('H*', '0FD1CEFACE'); + $bin2 = pack('H*', '0001020304'); + $bin3 = hex2bin('616263646566676869'); // abcdefghi + $bin4 = pack('H*', '7A61CC86C7BDCEB2F18FB3BF'); + + $xml = "The quick brown fox jumps over the lazy dog0123456789"; + + $c01 = [null, $str1, $str2]; + $c02 = [null, $longStr1, $longStr2]; + $c03 = [null, null, 999]; + $c04 = [null, 3.1415927, null]; + $c05 = [$bin1, null, $bin2]; + $c06 = [null, $bin3, $bin4]; + $c07 = [null, '1234.56', '9876.54']; + $c08 = [null, null, $xml]; + $c09 = [4.321, 'CF43B0B3-E645-48C4-9F25-1A2BB4CE581A', (0xCBAB)]; + + // Create a TVP input array + $nrows = 3; + $ncols = 8; + $params = array(); + for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($params, $rowValues); + } + + $tvpInput = array($tvpType => $params); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callSelectTVP2); + + // Bind parameters for the stored procedure + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + $stmt->execute(); + + // Verify the results + $row = 0; + while ($result = $stmt->fetch(PDO::FETCH_NUM)) { + // Compare the values against the inputs + for ($col = 0; $col < $ncols; $col++) { + if ($result[$col] != $params[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $params[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $result[$col] . PHP_EOL; + } + } + $row++; + } + unset($stmt); + + dropProc($conn, 'SelectTVP2'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt new file mode 100644 index 00000000..3c0116d1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt @@ -0,0 +1,162 @@ +--TEST-- +Test Table-valued parameter using bindParam and no null values +--DESCRIPTION-- +Test Table-valued parameter using bindParam and no null values. This test verifies the fetched results of the all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- += 80100) { + return verifyBinaryStream($image, $photo); + } else { + return verifyBinaryData($image, $photo); + } +} + +try { + date_default_timezone_set('America/Los_Angeles'); + + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + $custCode = 'PDO_123'; + $ordNo = 0; + $ordDate = null; + + $image1 = fopen($tvpIncPath. $gif1, 'rb'); + $image2 = fopen($tvpIncPath. $gif2, 'rb'); + $image3 = fopen($tvpIncPath. $gif3, 'rb'); + $images = [$image1, $image2, $image3]; + + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); + } + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntry); + + // Bind parameters for the stored procedure + $stmt->bindParam(1, $custCode); + $stmt->bindParam(2, $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(3, $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(4, $ordDate, PDO::PARAM_STR, 20); + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch a random inserted image from the table and verify them + $n = rand(10,100); + $index = $n % count($images); + + $tsql = 'SELECT Photo FROM TVPItem WHERE ItemNo = ' . ($index + 1); + $stmt = $conn->query($tsql); + $stmt->bindColumn('Photo', $photo, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + if ($row = $stmt->fetch(PDO::FETCH_BOUND)) { + if (!verifyPhoto($images[$index], $photo)) { + echo 'Image data corrupted for row '. ($index + 1) . PHP_EOL; + } + } else { + echo 'Failed in calling bindColumn' . PHP_EOL; + } + unset($photo); + + fclose($image1); + fclose($image2); + fclose($image3); + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch other basic types + $stmt = $conn->query($selectTVPItemQuery); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 +) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt new file mode 100644 index 00000000..76a42a23 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt @@ -0,0 +1,128 @@ +--TEST-- +Test Table-valued parameter using bindValue() and random null inputs +--DESCRIPTION-- +Test Table-valued parameter using bindValue() instead of bindParam() with random null values. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + dropProc($conn, 'SelectTVP'); + + $tvpType = 'TestTVP'; + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create table type and a stored procedure + $conn->exec($createTestTVP); + $conn->exec($createSelectTVP); + + // Create column arrays + $str = ''; + for ($i = 0; $i < 255; $i++) { + $str .= chr(($i % 95) + 32); + } + $longStr = str_repeat($str, 2000); + + $c01 = ['abcde', '', $str]; + $c02 = ['Hello world!', 'ABCDEFGHIJKLMNOP', $longStr]; + $c03 = [1, 0, 1]; + $c04 = [null, + null, + date_create('1955-12-13 12:20:00')]; + $c05 = [date_create('2384-12-31 12:40:12.34565'), null, date_create('1074-12-31 23:59:59.01234')]; + $c06 = ['4CDBC69F-F0EE-4963-8F17-24DD47090126', + '0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE', + null]; + $c07 = ['1234567', '-9223372036854775808', '9223372036854775807']; + $c08 = [null, -1.79E+308, 1.79E+308]; + $c09 = ['31234567890123.141243449787580175325274', + '0.000000000000000000000001', + '99999999999999.999999999999999999999999']; + + // Create a TVP input array + $nrows = 3; + $ncols = 9; + $params = array(); + for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($params, $rowValues); + } + + $tvpInput = array($tvpType => $params); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callSelectTVP); + + // Bind parameters for the stored procedure + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + $stmt->execute(); + + // Verify the results + $row = 0; + while ($result = $stmt->fetch(PDO::FETCH_NUM)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + if ($result[$col] != $params[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $params[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $result[$col] . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + var_dump($result[$col]); + } + echo PHP_EOL; + $row++; + } + unset($stmt); + + dropProc($conn, 'SelectTVP'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Row 1: from Col 3 to 9 +string(1) "1" +NULL +string(25) "2384-12-31 12:40:12.34565" +string(36) "4CDBC69F-F0EE-4963-8F17-24DD47090126" +string(7) "1234567" +NULL +string(39) "31234567890123.141243449787580175325274" + +Row 2: from Col 3 to 9 +string(1) "0" +NULL +NULL +string(36) "0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE" +string(20) "-9223372036854775808" +string(10) "-1.79E+308" +string(25) ".000000000000000000000001" + +Row 3: from Col 3 to 9 +string(1) "1" +string(19) "1955-12-13 12:20:00" +string(25) "1074-12-31 23:59:59.01234" +NULL +string(19) "9223372036854775807" +string(9) "1.79E+308" +string(39) "99999999999999.999999999999999999999999" + +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt new file mode 100644 index 00000000..d7320500 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt @@ -0,0 +1,148 @@ +--TEST-- +Test Table-valued parameter with a stored procedure that takes two TVPs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + + $conn->exec("DROP PROCEDURE [$schema].[AddReview]"); + $conn->exec("DROP TYPE [$schema].[TestTVP3]"); + $conn->exec("DROP TYPE [$schema].[SupplierType]"); + $conn->exec("DROP SCHEMA [$schema]"); + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } else { + global $dropSchema; + + $dropProcedure = dropProcSQL($conn, "[$schema].[AddReview]"); + $conn->exec($dropProcedure); + + $dropTableType = dropTableTypeSQL($conn, "TestTVP3", $schema); + $conn->exec($dropTableType); + $dropTableType = dropTableTypeSQL($conn, "SupplierType", $schema); + $conn->exec($dropTableType); + + $conn->exec($dropSchema); + } +} + +try { + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $stmt = $conn->query("SELECT @@VERSION"); + $result = $stmt->fetch(PDO::FETCH_NUM)[0]; + $version = explode(' ', $result); + $pre2016 = ($version[3] < '2016'); + + // Use a different schema instead of dbo + $schema = 'Sales DB'; + cleanup($conn, $schema, $pre2016); + + // Create the table type and stored procedure + $conn->exec($createSchema); + $conn->exec($createTestTVP3); + $conn->exec($createSupplierType); + $conn->exec($createAddReview); + + // Create the TVP input arrays + $inputs1 = [ + [12345, 'μεγάλο'], + [67890, 'μεσαία'], + [45678, 'μικρές'], + ]; + + $inputs2 = [ + ['abcde', 12345, '2019-12-31 23:59:59.123456'], + ['fghij', 67890, '2000-07-15 12:30:30.5678'], + ['klmop', 45678, '2007-04-08 06:15:15.333'], + ]; + + $tvpType1 = "SupplierType"; + $tvpType2 = "TestTVP3"; + + $tvpInput1 = array($tvpType1 => $inputs1, $schema); + $tvpInput2 = array($tvpType2 => $inputs2, $schema); + + $image = fopen($tvpIncPath. 'superlight_black_f_large.gif', 'rb'); + + $stmt = $conn->prepare($callAddReview); + $stmt->bindParam(1, $tvpInput1, PDO::PARAM_LOB); + $stmt->bindParam(2, $tvpInput2, PDO::PARAM_LOB); + $stmt->bindParam(3, $image, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + + // Verify the results + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + $stmt->nextRowset(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + $stmt->nextRowset(); + if ($row = $stmt->fetch(PDO::FETCH_NUM)) { + if (!verifyBinaryData($image, $row[0])) { + echo 'The image is corrupted' . PHP_EOL; + } + } else { + echo 'Something went wrong reading the image' . PHP_EOL; + } + + fclose($image); + unset($stmt); + + cleanup($conn, $schema, $pre2016); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Array +( + [SupplierId] => 12345 + [SupplierName] => μεγάλο +) +Array +( + [SupplierId] => 67890 + [SupplierName] => μεσαία +) +Array +( + [SupplierId] => 45678 + [SupplierName] => μικρές +) +Array +( + [SupplierId] => 12345 + [SalesDate] => 2019-12-31 23:59:59.1234560 + [Review] => abcde +) +Array +( + [SupplierId] => 67890 + [SalesDate] => 2000-07-15 12:30:30.5678000 + [Review] => fghij +) +Array +( + [SupplierId] => 45678 + [SalesDate] => 2007-04-08 06:15:15.3330000 + [Review] => klmop +) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt new file mode 100644 index 00000000..423e9033 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt @@ -0,0 +1,202 @@ +--TEST-- +Test various error cases with invalid Table-valued parameter inputs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +prepare($proc); + + // Bind TVP for the stored procedure + if ($inputParam) { + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + } else { + $stmt->bindParam(1, $tvpInput, PDO::PARAM_LOB, 100); + } + $stmt->execute(); + } catch (PDOException $e) { + echo "Error $caseNo: "; + echo $e->getMessage(); + echo PHP_EOL; + } +} + +function cleanup($conn, $schema, $tvpType, $procName, $pre2016) +{ + if ($pre2016) { + // ignore the errors dropping all these + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + + $conn->exec("DROP PROCEDURE [$schema].[$procName]"); + $conn->exec("DROP TYPE [$schema].[$tvpType]"); + $conn->exec("DROP SCHEMA [$schema]"); + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } else { + global $dropSchema; + + $dropProcedure = dropProcSQL($conn, "[$schema].[$procName]"); + $conn->exec($dropProcedure); + + $dropTableType = dropTableTypeSQL($conn, $tvpType, $schema); + $conn->exec($dropTableType); + + $conn->exec($dropSchema); + } +} + +try { + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $stmt = $conn->query("SELECT @@VERSION"); + $result = $stmt->fetch(PDO::FETCH_NUM)[0]; + $version = explode(' ', $result); + $pre2016 = ($version[3] < '2016'); + + // Use a different schema instead of dbo + $schema = 'Sales DB'; + $tvpTypeName = 'TestTVP3'; + $procName = 'SelectTVP3'; + + cleanup($conn, $schema, $tvpTypeName, $procName, $pre2016); + + // Create the table type and stored procedure + $conn->exec($createSchema); + $conn->exec($createTestTVP3); + $conn->exec($createSelectTVP3); + + // Create a TVP input array + $inputs = [ + ['ABC', 12345, null], + ['DEF', 6789, null], + ['GHI', null], + ]; + $str = 'dummy'; + + // Case (1) - do not provide TVP type name + $tvpInput = array($inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 1); + + // Case (2) - use an empty string as TVP type name + $tvpInput = array("" => array()); + invokeProc($conn, $callSelectTVP3, $tvpInput, 2); + + // Case (3) - null inputs + $tvpInput = array($tvpTypeName => null); + invokeProc($conn, $callSelectTVP3, $tvpInput, 3); + + // Case (4) - not using array as inputs + $tvpInput = array($tvpTypeName => 1); + invokeProc($conn, $callSelectTVP3, $tvpInput, 4); + + // Case (5) - invalid TVP type name + $tvpInput = array($str => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 5); + + // Case (6) - input rows are not the same size + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 6); + + // Case (7) - input row wrong size + unset($inputs); + $inputs = [ + ['ABC', 12345, null, null] + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 7); + + // Case (8) - use string keys + unset($inputs); + $inputs = [ + ['A' => null, null, null] + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 8); + + // Case (9) - a row is not an array + unset($inputs); + $inputs = [null]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 9); + + // Case (10) - a column value used a string key + unset($inputs); + $inputs = [ + ['ABC', 12345, "key"=>null] + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 10); + + // Case (11) - invalid input object for a TVP column + class foo + { + function do_foo(){} + } + $bar = new foo; + unset($inputs); + $inputs = [ + ['ABC', 1234, $bar], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 11); + + // Case (12) - invalid input type for a TVP column + unset($inputs); + $inputs = [ + ['ABC', &$str, null], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 12); + + // Case (13) - bind a TVP as an OUTPUT param + invokeProc($conn, $callSelectTVP3, $tvpInput, 13, false); + + // Case (14) - test UTF-8 invalid/corrupt string for a TVP column + unset($inputs); + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395e38395"; + $utf8 = substr_replace($utf8, "fe", 1000, 2); + $utf8 = pack("H*", $utf8); + + $inputs = [ + [$utf8, 1234, null], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs, $schema); + invokeProc($conn, $callSelectTVP3, $tvpInput, 14); + + cleanup($conn, $schema, $tvpTypeName, $procName, $pre2016); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECTF-- +Error 1: SQLSTATE[IMSSP]: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 2: SQLSTATE[IMSSP]: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 3: SQLSTATE[IMSSP]: Invalid inputs for Table-Valued Param 1 +Error 4: SQLSTATE[IMSSP]: Invalid inputs for Table-Valued Param 1 +Error 5: SQLSTATE[IMSSP]: Failed to get metadata for Table-Valued Param 1 +Error 6: SQLSTATE[IMSSP]: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 7: SQLSTATE[IMSSP]: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 8: SQLSTATE[IMSSP]: Associative arrays not allowed for Table-Valued Param 1 +Error 9: SQLSTATE[IMSSP]: Expect an array for each row for Table-Valued Param 1 +Error 10: SQLSTATE[IMSSP]: Associative arrays not allowed for Table-Valued Param 1 +Error 11: SQLSTATE[IMSSP]: An invalid type for Table-Valued Param 1 Column 3 was specified +Error 12: SQLSTATE[IMSSP]: An invalid type for Table-Valued Param 1 Column 2 was specified +Error 13: SQLSTATE[IMSSP]: You cannot return data in a table-valued parameter. Table-valued parameters are input-only. +Error 14: SQLSTATE[IMSSP]: An error occurred translating a string for Table-Valued Param 1 Column 1 to UTF-16: %a +Done diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt new file mode 100644 index 00000000..4605d882 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt @@ -0,0 +1,160 @@ +--TEST-- +Table-valued parameter with bindParam and named parameters. The initial values of a column are NULLs +--DESCRIPTION-- +Test Table-valued parameter with bindParam. The initial values of a column are NULLs. This test verifies the fetched results using client buffers. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- += 80100) { + return verifyBinaryStream($image, $photo); + } else { + return verifyBinaryData($image, $photo); + } +} + +try { + date_default_timezone_set('America/Los_Angeles'); + + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + // Bind parameters for call to TVPOrderEntry + $custCode = 'PDO_789'; + $ordNo = 0; + $ordDate = null; + + // TVP supports column-wise binding + $image3 = fopen($tvpIncPath. $gif3, 'rb'); + $images = [null, null, $image3]; + + // Added images to $items + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); + } + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntryNamed); + + $stmt->bindParam(':id', $custCode); + $stmt->bindParam(':tvp', $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(':ordNo', $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(':ordDate', $ordDate, PDO::PARAM_STR, 20); + + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch the only image from the table that is not NULL + $tsql = 'SELECT ItemNo, Photo FROM TVPItem WHERE Photo IS NOT NULL ORDER BY ItemNo'; + $stmt = $conn->query($tsql); + $index = 2; + $stmt->bindColumn('Photo', $photo, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + + if ($row = $stmt->fetch(PDO::FETCH_BOUND)) { + if (!verifyPhoto($images[$index], $photo)) { + echo 'Image data corrupted for row '. ($index + 1) . PHP_EOL; + } + } else { + echo 'Failed in calling bindColumn' . PHP_EOL; + } + unset($photo); + fclose($image3); + + // Fetch other basic types + $stmt = $conn->prepare($selectTVPItemQuery, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 +) +Done diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt new file mode 100644 index 00000000..311bbaa2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt @@ -0,0 +1,162 @@ +--TEST-- +Test Table-valued parameter using bindParam and some NULL inputs +--DESCRIPTION-- +Test Table-valued parameter using bindParam with some NULL input values. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + // Bind parameters for call to TVPOrderEntry + $custCode = 'PDO_456'; + $ordNo = 0; + $ordDate = null; + + // Add null image to $items + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], null); + } + + // Randomly set some values to null + $items[1][0] = null; + $items[0][2] = null; + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntryNamed); + + $stmt->bindParam(':id', $custCode); + $stmt->bindParam(':tvp', $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(':ordNo', $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(':ordDate', $ordDate, PDO::PARAM_STR, 20); + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch all columns + $tsql = 'SELECT * FROM TVPItem ORDER BY ItemNo'; + $stmt = $conn->query($tsql); + if ($row = $stmt->fetchall(PDO::FETCH_NUM)) { + var_dump($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +array(3) { + [0]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "1" + [2]=> + string(10) "0062836700" + [3]=> + string(3) "367" + [4]=> + NULL + [5]=> + string(18) "AWC Tee Male Shirt" + [6]=> + string(5) "20.75" + [7]=> + NULL + } + [1]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "2" + [2]=> + NULL + [3]=> + string(3) "256" + [4]=> + string(10) "2017-11-07" + [5]=> + string(24) "Superlight Black Bicycle" + [6]=> + string(6) "998.45" + [7]=> + NULL + } + [2]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "3" + [2]=> + string(10) "1328781505" + [3]=> + string(3) "260" + [4]=> + string(10) "2010-03-03" + [5]=> + string(22) "Silver Chain for Bikes" + [6]=> + string(5) "88.98" + [7]=> + NULL + } +} +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_warnings.phpt b/test/functional/pdo_sqlsrv/pdo_warnings.phpt index e83b766b..b0e9a1dc 100644 --- a/test/functional/pdo_sqlsrv/pdo_warnings.phpt +++ b/test/functional/pdo_sqlsrv/pdo_warnings.phpt @@ -4,12 +4,38 @@ Test warnings on connection and statement levels --FILE-- getAttribute(PDO::ATTR_TIMEOUT); + restore_error_handler(); $tbname = "table1"; dropTable($conn, $tbname); @@ -22,15 +48,7 @@ try { unset($conn); } catch (PDOException $e) { var_dump($e->errorInfo); - exit; } ?> --EXPECTREGEX-- -Warning: SQLSTATE: IMSSP -Error Code: -38 -Error Message: An unsupported attribute was designated on the PDO object\. - in .+(\/|\\)pdo_warnings\.php on line [0-9]+ - -Warning: PDO::getAttribute\(\): SQLSTATE\[IM001\]: Driver does not support this function: driver does not support that attribute in .+(\/|\\)pdo_warnings\.php on line [0-9]+ - Warning: PDOStatement::execute\(\): SQLSTATE\[42000\]: Syntax error or access violation: 156 \[Microsoft\]\[ODBC Driver 1[1-9] for SQL Server\]\[SQL Server\]Incorrect syntax near the keyword 'TABLE'\. in .+(\/|\\)pdo_warnings\.php on line [0-9]+ diff --git a/test/functional/pdo_sqlsrv/pdostatement_Buffqry.phpt b/test/functional/pdo_sqlsrv/pdostatement_Buffqry.phpt index 31e36a88..e453fcf7 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_Buffqry.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_Buffqry.phpt @@ -111,8 +111,10 @@ function bindPARAM_NULL($db, $tbname) $query = "UPDATE PDO_AllTypes SET [BitCol]=:Name WHERE [VarcharCol]=:value"; $stmt = $db->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); - fwrite($noteID, null); - rewind($noteID); + // PHP Deprecated: fwrite(): Passing null to parameter #2 ($data) of type string is deprecated + // Also, it is not necessary for this test + // fwrite($noteID, null); + // rewind($noteID); $stmt->bindParam(':Name', $noteID, PDO::PARAM_NULL); $stmt->bindParam(':value', $data); $stmt->execute(); diff --git a/test/functional/pdo_sqlsrv/pdostatement_debugDumpParams.phpt b/test/functional/pdo_sqlsrv/pdostatement_debugDumpParams.phpt index 64364ec3..deba3dbb 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_debugDumpParams.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_debugDumpParams.phpt @@ -28,16 +28,16 @@ try { var_dump($e); } ?> ---EXPECT-- +--EXPECTF-- SQL: [79] SELECT IntCol FROM PDO_AllTypes WHERE BigIntCol = :bigint AND CharCol = :string Params: 2 Key: Name: [7] :bigint paramno=0 name=[7] ":bigint" is_param=1 -param_type=1 +param_type=%d Key: Name: [7] :string paramno=1 name=[7] ":string" is_param=1 -param_type=2 +param_type=%d diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt index 8791a96b..bc21e3dc 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt @@ -185,7 +185,7 @@ try { } ?> ---EXPECT-- +--EXPECTF-- SQLSTATE[IMSSP]: The given attribute is only supported on the PDOStatement object. SQLSTATE[IMSSP]: An invalid attribute was designated on the PDOStatement object. Start inserting data... @@ -271,7 +271,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(6) "Budget" ["len"]=> diff --git a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt index bf54175a..75bfeca9 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt @@ -94,7 +94,7 @@ try { ?> ---EXPECT-- +--EXPECTF-- array(8) { ["flags"]=> @@ -106,7 +106,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(6) "IntCol" ["len"]=> @@ -124,7 +124,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(7) "CharCol" ["len"]=> @@ -142,7 +142,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(8) "NCharCol" ["len"]=> @@ -160,7 +160,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(11) "DateTimeCol" ["len"]=> @@ -178,7 +178,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(10) "VarcharCol" ["len"]=> @@ -196,7 +196,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(11) "NVarCharCol" ["len"]=> @@ -214,7 +214,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(8) "FloatCol" ["len"]=> @@ -230,7 +230,7 @@ array(7) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(6) "XmlCol" ["len"]=> diff --git a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt index f3485ec6..fa7e015b 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt @@ -133,7 +133,7 @@ try { exit; } ?> ---EXPECT-- +--EXPECTF-- array(8) { ["flags"]=> @@ -145,7 +145,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(12) "此是後話" ["len"]=> @@ -163,7 +163,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(29) "Κοντάוְאַתָּה第" ["len"]=> @@ -181,7 +181,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(30) "NΚοντάוְאַתָּה第" ["len"]=> @@ -199,7 +199,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(38) "ნომინავიiałopioБун" ["len"]=> @@ -217,7 +217,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(10) "VarcharCol" ["len"]=> @@ -235,7 +235,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(33) "NVarΚοντάוְאַתָּה第" ["len"]=> @@ -253,7 +253,7 @@ array(8) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(8) "FloatCol" ["len"]=> @@ -269,7 +269,7 @@ array(7) { ["table"]=> string(0) "" ["pdo_type"]=> - int(2) + int(%d) ["name"]=> string(6) "XmlCol" ["len"]=> diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index 195430d5..ddefaac6 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -11,6 +11,10 @@ require_once('MsHelper.inc'); require_once('MsSetup.inc'); +$tvpIncPath = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'inc'.DIRECTORY_SEPARATOR; + +require_once($tvpIncPath. 'test_tvp_data.php'); + $usingUTF8data = false; function isWindows() @@ -559,4 +563,19 @@ function verifyError($error, $state, $message) } } +function getTodayDateAsString($conn) +{ + $tsql = 'SELECT CONVERT (VARCHAR(20), GETDATE())'; + $stmt = sqlsrv_query($conn, $tsql); + $result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC); + $today = ''; + if ($result) { + $today = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + } else { + echo "Failed to get today's date as string: " . PHP_EOL; + print_r(sqlsrv_errors()); + } + + return $today; +} ?> diff --git a/test/functional/sqlsrv/MsHelper.inc b/test/functional/sqlsrv/MsHelper.inc index 590e8b33..cdff8764 100644 --- a/test/functional/sqlsrv/MsHelper.inc +++ b/test/functional/sqlsrv/MsHelper.inc @@ -33,6 +33,7 @@ class ColumnMeta public $options; // a string that is null by default (e.g. NOT NULL Identity (1,1) ) protected $encryptable; // whether Always Encrypted supports this column + protected $forcedEncrypt; // force column encryption regardless, default to 'false' public function __construct($dataType, $colName = null, $options = null, $deterministic = true, $noEncrypt = false) { @@ -41,6 +42,8 @@ class ColumnMeta } else { $this->colName = $colName; } + + $this->forcedEncrypt = false; $this->encType = ($deterministic ? "deterministic" : "randomized"); if (empty($dataType)) { @@ -71,7 +74,7 @@ class ColumnMeta $unsupported = array("xml", "timestamp", "image", "ntext", "text", "sql_variant", "hierarchyid", "geography", "geometry", "alias"); - if (stripos($this->options, "identity") !== false) { + if (!is_null($this->options) && stripos($this->options, "identity") !== false) { $this->encryptable = false; } elseif (in_array(strtolower($this->dataType), $unsupported)) { $this->encryptable = false; @@ -79,6 +82,16 @@ class ColumnMeta $this->encryptable = true; } } + + /** + * force column to be encrypted regardless of the current settings + * @return void + */ + public function forceEncryption($forceEncryption) + { + $this->forcedEncrypt = $forceEncryption; + } + /** * @return string column definition for creating a table */ @@ -86,9 +99,12 @@ class ColumnMeta { $append = " "; - if ($this->encryptable && isDataEncrypted()) { + if (($this->encryptable && isDataEncrypted()) || $this->forcedEncrypt) { $cekName = getCekName(); + if ($this->forcedEncrypt && empty($cekName)) { + $cekName = 'AEColumnKey'; // Use Windows AE key by default + } if (stripos($this->dataType, "char") !== false) { $append .= "COLLATE Latin1_General_BIN2 "; } @@ -164,7 +180,7 @@ class BindParamOption $scal = $prec_scal[1]; $size = null; } - if (strpos($size, "max") !== false) { + if (!is_null($size) && strpos($size, "max") !== false) { $size = trim($size, "'"); } } @@ -724,7 +740,11 @@ function insertTestRow($conn, $tbname, $index) $value = $value[0]; // this might be an input to a decimal, a numeric or a binary field if (isBinary($col)) { - $value = "0x" . $value; // annotate the input string as a hex string + if (!is_null($value)) { + $value = "0x" . $value; // annotate the input string as a hex string + } else { + $value = null; + } } } if (is_null($value)) { @@ -778,15 +798,14 @@ function getInsertData($rowIndex, $colIndex) if (empty($inputArray)) { fatalError("getInsertData: failed to retrieve data at row $rowIndex.\n"); } - $count = 0; - foreach ($inputArray as $key => $value) { - if (++$count == $colIndex) { - if (is_array($value)) { - return $value[0]; - } else { - return $value; - } - break; + + $key = getColName($colIndex); + if (!empty($key)) { + $value = $inputArray[$key]; + if (is_array($value)) { + return $value[0]; + } else { + return $value; } } @@ -972,11 +991,7 @@ function isUnicode($k) function isUpdatable($k) { - switch ($k) { - case 27: return (false); // timestamp - default: break; - } - return (true); + return ($k != 27); // timestamp } function isLiteral($k) diff --git a/test/functional/sqlsrv/TC32_DeleteQuery.phpt b/test/functional/sqlsrv/TC32_DeleteQuery.phpt index e7ea92fb..68a43827 100644 --- a/test/functional/sqlsrv/TC32_DeleteQuery.phpt +++ b/test/functional/sqlsrv/TC32_DeleteQuery.phpt @@ -37,7 +37,7 @@ function deleteQuery() trace("Deleting rows from $tableName ..."); $delRows = 1; - if (strlen($keyValue) == 0) { + if (empty($keyValue)) { $stmt2 = AE\executeQuery($conn1, "DELETE TOP(1) FROM [$tableName]"); $cond = "(top row)"; } else { diff --git a/test/functional/sqlsrv/TC43_FetchData.phpt b/test/functional/sqlsrv/TC43_FetchData.phpt index ad446f0c..23bf92b8 100644 --- a/test/functional/sqlsrv/TC43_FetchData.phpt +++ b/test/functional/sqlsrv/TC43_FetchData.phpt @@ -49,8 +49,11 @@ function fetchFields() if (isUpdatable($col)) { // should check data even if $fld is null $data = AE\getInsertData($startRow + $i, $col); - if (!checkData($col, $fld, $data)) { - echo("\nData error\nExpected:\n$data\nActual:\n$fld\n"); + if (!checkData($col, $fld, $data, isBinary($col))) { + echo("\nData error\nExpected:\n"); + var_dump($data); + echo("\nActual:\n"); + var_dump($fld); setUTF8Data(false); die("Data corruption on row ".($startRow + $i)." column $col"); @@ -66,10 +69,23 @@ function fetchFields() sqlsrv_close($conn1); } -function checkData($col, $actual, $expected) +function checkData($col, $actual, $expected, $isBinary) { $success = true; - + + // First check for nulls + if (is_null($expected)) { + $success = is_null($actual); + if (!$success) { + trace("\nData error\nExpected null but Actual:\n$actual\n"); + } + return $success; + } elseif (is_null($actual)) { + trace("\nData error\nExpected:\n$expected\nbut Actual is null\n"); + return false; + } + + // Neither is null, so keep checking if (isNumeric($col)) { if (floatval($actual) != floatval($expected)) { $success = false; diff --git a/test/functional/sqlsrv/TC44_FetchArray.phpt b/test/functional/sqlsrv/TC44_FetchArray.phpt index b6073f6b..13d9336e 100644 --- a/test/functional/sqlsrv/TC44_FetchArray.phpt +++ b/test/functional/sqlsrv/TC44_FetchArray.phpt @@ -126,9 +126,17 @@ function checkData($row, $stmt, $index, $mode) } } elseif (isBinary($col)) { $expected = sqlsrv_get_field($stmt, $index, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); - $actual = bin2hex($actual); - if (strcasecmp($actual, $expected) != 0) { - $success = false; + if (is_null($expected)) { + $success = is_null($actual); + } else { + if (is_null($actual)) { + $success = false; + } else { + $actual = bin2hex($actual); + if (strcasecmp($actual, $expected) != 0) { + $success = false; + } + } } } else { // if (isChar($col)) if (useUTF8Data()) { @@ -136,7 +144,9 @@ function checkData($row, $stmt, $index, $mode) } else { $expected = sqlsrv_get_field($stmt, $index, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); } - if (strcmp($actual, $expected) != 0) { + if (is_null($expected)) { + $success = is_null($actual); + } elseif (strcmp($actual, $expected) != 0) { $success = false; } } diff --git a/test/functional/sqlsrv/TC51_StreamRead.phpt b/test/functional/sqlsrv/TC51_StreamRead.phpt index 967aab1d..565e41fe 100644 --- a/test/functional/sqlsrv/TC51_StreamRead.phpt +++ b/test/functional/sqlsrv/TC51_StreamRead.phpt @@ -86,15 +86,24 @@ function verifyStream($stmt, $row, $colIndex) function checkData($col, $actual, $expected) { + if (is_null($expected)) { + return empty($actual); + } + $success = true; - + if (isBinary($col)) { - $actual = bin2hex($actual); - if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + if (is_null($actual)) { $success = false; + } else { + $actual = bin2hex($actual); + if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + $success = false; + } } } else { - if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + $len = (empty($expected)) ? 0 : strlen($expected); + if (strncasecmp($actual, $expected, $len) != 0) { if ($col != 19) { // skip ntext $pos = strpos($actual, $expected); if (($pos === false) || ($pos > 1)) { diff --git a/test/functional/sqlsrv/TC52_StreamSend.phpt b/test/functional/sqlsrv/TC52_StreamSend.phpt index 3779e444..383f48d4 100644 --- a/test/functional/sqlsrv/TC52_StreamSend.phpt +++ b/test/functional/sqlsrv/TC52_StreamSend.phpt @@ -13,15 +13,25 @@ PHPT_EXEC=true 'UTF-8')); + } else { + $conn1 = AE\connect(); + } + + $factor = 500; for ($k = $minType; $k <= $maxType; $k++) { switch ($k) { @@ -53,6 +63,7 @@ function sendStream($minType, $maxType, $atExec) case 14:// varchar(max) case 17:// nvarchar(max) $data = "The quick brown fox jumps over the lazy dog 0123456789"; + $data = str_repeat($data, $factor); break; case 18:// text @@ -70,10 +81,12 @@ function sendStream($minType, $maxType, $atExec) case 22:// varbinary(max) $data = "98765432100123456789"; + $data = str_repeat($data, $factor); break; case 23:// image $data = "01234567899876543210"; + $data = str_repeat($data, $factor); $phpType = SQLSRV_SQLTYPE_IMAGE; break; @@ -94,7 +107,7 @@ function sendStream($minType, $maxType, $atExec) die("Unknown data type: $k."); break; } - + if ($data != null) { $fname1 = fopen($fileName, "w"); fwrite($fname1, $data); @@ -168,8 +181,10 @@ function checkData($conn, $table, $cols, $expectedValue) } try { - sendStream(12, 28, true); // send stream at execution - sendStream(12, 28, false); // send stream after execution + sendStream(12, 28, true, false); // send stream at execution + sendStream(12, 28, false, false); // send stream after execution + sendStream(12, 28, true, true); // send stream at execution (UTF-8) + sendStream(12, 28, false, true); // send stream after execution (UTF-8) } catch (Exception $e) { echo $e->getMessage(); } @@ -178,3 +193,5 @@ try { --EXPECT-- Test "Stream - Send at Execution" completed successfully. Test "Stream - Send after Execution" completed successfully. +Test "Stream - Send at Execution (UTF-8)" completed successfully. +Test "Stream - Send after Execution (UTF-8)" completed successfully. diff --git a/test/functional/sqlsrv/TC55_StreamScrollable.phpt b/test/functional/sqlsrv/TC55_StreamScrollable.phpt index f3c88b1f..4fde1464 100644 --- a/test/functional/sqlsrv/TC55_StreamScrollable.phpt +++ b/test/functional/sqlsrv/TC55_StreamScrollable.phpt @@ -105,15 +105,24 @@ function verifyStream($stmt, $row, $colIndex) function checkData($col, $actual, $expected) { + if (is_null($expected)) { + return empty($actual); + } + $success = true; if (isBinary($col)) { - $actual = bin2hex($actual); - if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + if (is_null($actual)) { $success = false; + } else { + $actual = bin2hex($actual); + if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + $success = false; + } } } else { - if (strncasecmp($actual, $expected, strlen($expected)) != 0) { + $len = (empty($expected)) ? 0 : strlen($expected); + if (strncasecmp($actual, $expected, $len) != 0) { if ($col != 19) { // skip ntext $pos = strpos($actual, $expected); diff --git a/test/functional/sqlsrv/sqlsrv_2008_dataTypes.phpt b/test/functional/sqlsrv/sqlsrv_2008_dataTypes.phpt index f7fa0151..eef68e38 100644 --- a/test/functional/sqlsrv/sqlsrv_2008_dataTypes.phpt +++ b/test/functional/sqlsrv/sqlsrv_2008_dataTypes.phpt @@ -25,13 +25,14 @@ if (!$stmt) { } // insert new date time types as strings (this works now) +$d1 = date_create(); $insertSql = "INSERT INTO [$tableName] (id, [c1_date], [c2_time], [c3_datetimeoffset], [c4_datetime2]) VALUES (?, ?, ?, ?, ?)"; $stmt = AE\executeQueryParams( $conn, $insertSql, array(rand(0, 99999), - array(strftime('%Y-%m-%d'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_DATE), - array(strftime('%H:%M:%S'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_TIME), + array(date_format($d1, 'Y-m-d'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_DATE), + array(date_format($d1, 'H:i:s'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_TIME), array(date_format(date_create(), 'Y-m-d H:i:s.u P'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_DATETIMEOFFSET), array(date_format(date_create(), 'Y-m-d H:i:s.u'), SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'), SQLSRV_SQLTYPE_DATETIME2)), false, @@ -61,10 +62,11 @@ $stmt = AE\executeQueryParams( ); // insert new date time types as strings with no type information (this works) +$d2 = date_create(); $stmt = AE\executeQueryParams( $conn, $insertSql, - array(rand(0, 99999), strftime('%Y-%m-%d'), strftime('%H:%M:%S'), date_format(date_create(), 'Y-m-d H:i:s.u P'), date_format(date_create(), 'Y-m-d H:i:s.u P')), + array(rand(0, 99999), date_format($d2, 'Y-m-d'), date_format($d2, 'H:i:s'), date_format(date_create(), 'Y-m-d H:i:s.u P'), date_format(date_create(), 'Y-m-d H:i:s.u P')), false, "Insert 4 failed" ); diff --git a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt index c492a961..3c47a00a 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt @@ -1,9 +1,24 @@ --TEST-- Test insert data and fetch as all possible php types --DESCRIPTION-- -Test insert data of most common column types and fetch them all as possible php types +Test insert data of most common column types and fetch them all as possible php types. +This test requires the Always Encrypted feature. --SKIPIF-- - + $database, "UID" => $userName, "PWD" => $userPassword); +$conn = sqlsrv_connect($server, $options); +if (! $conn) { + die("Skip Could not connect during SKIPIF!"); +} +if (!AE\isQualified($conn)) { + die("skip AE feature not supported in the current environment."); +} +?> --FILE-- forceEncryption($forceEncryption); + $columns[] = $anAEcolumn; $columns[] = new AE\ColumnMeta($dataTypes[$i], "c_".$colname, null, true, true); $queryTypes .= "c_"."$colname, "; $queryTypes .= "c_"."$colname"."_AE, "; @@ -66,7 +86,7 @@ set_time_limit(0); sqlsrv_configure('WarningsReturnAsErrors', 1); // Connect -$connectionInfo = array("CharacterSet"=>"UTF-8"); +$connectionInfo = array('CharacterSet'=>'UTF-8', 'ColumnEncryption' => 'Enabled'); $conn = AE\connect($connectionInfo); if (!$conn) { fatalError("Could not connect.\n"); diff --git a/test/functional/sqlsrv/sqlsrv_ae_type_conversion_select.phpt b/test/functional/sqlsrv/sqlsrv_ae_type_conversion_select.phpt index d4ea1c94..f1571dbc 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_type_conversion_select.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_type_conversion_select.phpt @@ -1,13 +1,30 @@ --TEST-- Test fetching data by conversion with CAST in the SELECT statement --DESCRIPTION-- -This test checks the allowed data type conversions in SELECT statements under Always Encrypted and non-encrypted +This test requires the Always Encrypted feature and checks the allowed data type conversions in +SELECT statements under Always Encrypted and non-encrypted Reference chart for conversions found at https://www.microsoft.com/en-us/download/details.aspx?id=35834 --SKIPIF-- - + $database, "UID" => $userName, "PWD" => $userPassword); +$conn = sqlsrv_connect($server, $options); +if (! $conn) { + die("Skip Could not connect during SKIPIF!"); +} +if (!AE\isQualified($conn)) { + die("skip AE feature not supported in the current environment."); +} +?> --FILE-- forceEncryption($forceEncryption); + $columns[] = $anAEcolumn; + $columns[] = new AE\ColumnMeta($dataTypes[$i], "c_".$colname, null, true, true); + $queryTypes .= "c_"."$colname, "; + $queryTypes .= "c_"."$colname"."_AE, "; + $valuesString .= "?, ?, "; + } + + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; +} + // Build the select queries. We want every combination of types for conversion // testing, so the matrix of queries selects every type from every column // and convert using CAST. @@ -154,7 +201,7 @@ $conversionMatrixAE = array(array('y','y','y','x','x','x','x','x','x','x','x','x set_time_limit(0); sqlsrv_configure('WarningsReturnAsErrors', 1); -$connectionInfo = array("CharacterSet"=>"UTF-8"); +$connectionInfo = array('CharacterSet'=>'UTF-8', 'ColumnEncryption' => 'Enabled'); $conn = AE\connect($connectionInfo); if (!$conn) { fatalError("Could not connect.\n"); diff --git a/test/functional/sqlsrv/sqlsrv_client_info.phpt b/test/functional/sqlsrv/sqlsrv_client_info.phpt index e809367b..d39819f9 100644 --- a/test/functional/sqlsrv/sqlsrv_client_info.phpt +++ b/test/functional/sqlsrv/sqlsrv_client_info.phpt @@ -21,5 +21,5 @@ array\(4\) { \[\"DriverVer\"\]=> string\(10\) \"[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{4}\" \[\"ExtensionVer\"\]=> - string\([0-9]+\) \"[0-9]\.[0-9]\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)?\" + string\([0-9]+\) \"[0-9]\.[0-9]+\.[0-9](-(RC[1-9]?|beta[1-9]))?(\.[0-9]+)?(\+[0-9]+)?\" } \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_connect_driver.phpt b/test/functional/sqlsrv/sqlsrv_connect_driver.phpt index cc90419f..ee78f0a4 100644 --- a/test/functional/sqlsrv/sqlsrv_connect_driver.phpt +++ b/test/functional/sqlsrv/sqlsrv_connect_driver.phpt @@ -106,7 +106,7 @@ function testEncryptedWithODBC($msodbcsqlMaj, $server, $connectionOptions) $connectionOptions['Driver']=$value; $connectionOptions['ColumnEncryption']='Enabled'; - $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; + $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server"; connectVerifyOutput($server, $connectionOptions, $expected); } diff --git a/test/functional/sqlsrv/sqlsrv_data_types_fetch_binary_stream.phpt b/test/functional/sqlsrv/sqlsrv_data_types_fetch_binary_stream.phpt index 56924648..3e425f4b 100644 --- a/test/functional/sqlsrv/sqlsrv_data_types_fetch_binary_stream.phpt +++ b/test/functional/sqlsrv/sqlsrv_data_types_fetch_binary_stream.phpt @@ -60,7 +60,12 @@ function FetchAsStream_Binary() function CompareValues($actual, $expected) { - return (strncasecmp($actual, $expected, strlen($expected)) === 0); + if (is_null($expected)) { + return (is_null($actual)); + } + + $len = (empty($expected)) ? 0 : strlen($expected); + return (strncasecmp($actual, $expected, $len) === 0); } function GetQuery($tableName, $index) diff --git a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt index 92b7f0ae..e751df8b 100644 --- a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt +++ b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt @@ -11,21 +11,8 @@ require_once("MsCommon.inc"); // These are the error messages we expect at various points below $errorNoMoreResults = "There are no more results returned by the query."; -$errorNoMoreRows = "There are no more rows in the active result set. Since this result set is not scrollable, no more data may be retrieved."; $errorNoFields = "The active result for the query contains no fields."; -// Variable function gets an error message that depends on the OS -function getFuncSeqError() -{ - if ( strtoupper( substr( php_uname( 's' ),0,3 ) ) === 'WIN' ) { - return "[Microsoft][ODBC Driver Manager] Function sequence error"; - } else { - return "[unixODBC][Driver Manager]Function sequence error"; - } -} - -$errorFuncSeq = 'getFuncSeqError'; - // This function takes an array of expected error messages and compares the // contents to the actual errors function CheckError($expectedErrors) @@ -99,16 +86,16 @@ echo "Nonempty result set, call fetch first: ###############################\n"; $stmt = sqlsrv_query($conn,"TestEmptySetProc @a='a', @b='b'"); Fetch($stmt, []); NextResult($stmt, []); -Fetch($stmt, [$errorFuncSeq()]); -NextResult($stmt, [$errorNoMoreResults, $errorFuncSeq()]); +Fetch($stmt, [$errorNoMoreResults]); +NextResult($stmt, [$errorNoMoreResults]); // Call next_result on a nonempty result set echo "Nonempty result set, call next_result first: #########################\n"; $stmt = sqlsrv_query($conn,"TestEmptySetProc @a='a', @b='b'"); NextResult($stmt, []); -Fetch($stmt, [$errorFuncSeq()]); -NextResult($stmt, [$errorNoMoreResults, $errorFuncSeq()]); +Fetch($stmt, [$errorNoMoreResults]); +NextResult($stmt, [$errorNoMoreResults]); // Call next_result twice in succession on a nonempty result set echo "Nonempty result set, call next_result twice: #########################\n"; @@ -123,7 +110,7 @@ echo "Empty result set, call fetch first: ##################################\n"; $stmt = sqlsrv_query($conn,"TestEmptySetProc @a='a', @b='w'"); Fetch($stmt, []); NextResult($stmt, []); -Fetch($stmt, [$errorNoMoreRows]); +Fetch($stmt, [$errorNoMoreResults]); NextResult($stmt, [$errorNoMoreResults]); // Call next_result on an empty result set @@ -131,8 +118,8 @@ echo "Empty result set, call next_result first: ############################\n"; $stmt = sqlsrv_query($conn,"TestEmptySetProc @a='a', @b='w'"); NextResult($stmt, []); -Fetch($stmt, [$errorFuncSeq()]); -NextResult($stmt, [$errorNoMoreResults, $errorFuncSeq()]); +Fetch($stmt, [$errorNoMoreResults]); +NextResult($stmt, [$errorNoMoreResults]); // Call next_result twice in succession on an empty result set echo "Empty result set, call next_result twice: ############################\n"; @@ -153,7 +140,7 @@ echo "Null result set, call next result first: #############################\n"; $stmt = sqlsrv_query($conn, "TestEmptySetProc @a='a', @b='c'"); NextResult($stmt, []); -Fetch($stmt, [$errorNoFields]); +Fetch($stmt, [$errorNoMoreResults]); // Call next_result twice in succession on a null result set echo "Null result set, call next result twice: #############################\n"; diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt new file mode 100644 index 00000000..39968110 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt @@ -0,0 +1,147 @@ +--TEST-- +Test Table-valued parameter with a stored procedure that takes two TVPs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8', 'ReturnDatesAsStrings' => true)); + +$stmt = sqlsrv_query($conn, "SELECT @@VERSION"); +if (sqlsrv_fetch($stmt)) { + $result = sqlsrv_get_field($stmt, 0); +} +$version = explode(' ', $result); +$pre2016 = ($version[3] < '2016'); + +// Use a different schema instead of dbo +$schema = 'Sales DB'; +cleanup($conn, $schema, $pre2016); + +// Create table types and stored procedures +sqlsrv_query($conn, $createSchema); +sqlsrv_query($conn, $createTestTVP3); +sqlsrv_query($conn, $createSupplierType); +sqlsrv_query($conn, $createAddReview); + +// Create the TVP input arrays +$inputs1 = [ + [12345, 'Large大'], + [67890, 'Medium中'], + [45678, 'Small小'], +]; + +$inputs2 = [ + ['ABCDE', 12345, '2019-12-31 23:59:59.123456'], + ['FGHIJ', 67890, '2000-07-15 12:30:30.5678'], + ['KLMOP', 45678, '2007-04-08 06:15:15.333'], +]; + +$tvpType1 = "SupplierType"; +$tvpType2 = "TestTVP3"; + +$tvpInput1 = array($tvpType1 => $inputs1, $schema); +$tvpInput2 = array($tvpType2 => $inputs2, $schema); + +$image = fopen($tvpIncPath. 'awc_tee_male_large.gif', 'rb'); + +$params = array(array($tvpInput1, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array($tvpInput2, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array($image, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('MAX'))); + +$stmt = sqlsrv_query($conn, $callAddReview, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + print_r($result); +} +sqlsrv_next_result($stmt); +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + print_r($result); +} +sqlsrv_next_result($stmt); +if (sqlsrv_fetch($stmt)) { + $photo = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($image, $photo)) { + echo 'Image data is corrupted' . PHP_EOL; + } +} else { + echo 'Something went wrong reading the image' . PHP_EOL; +} + +fclose($image); +cleanup($conn, $schema, $pre2016); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Array +( + [SupplierId] => 12345 + [SupplierName] => Large大 +) +Array +( + [SupplierId] => 67890 + [SupplierName] => Medium中 +) +Array +( + [SupplierId] => 45678 + [SupplierName] => Small小 +) +Array +( + [SupplierId] => 12345 + [SalesDate] => 2019-12-31 23:59:59.1234560 + [Review] => ABCDE +) +Array +( + [SupplierId] => 67890 + [SalesDate] => 2000-07-15 12:30:30.5678000 + [Review] => FGHIJ +) +Array +( + [SupplierId] => 45678 + [SalesDate] => 2007-04-08 06:15:15.3330000 + [Review] => KLMOP +) +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt new file mode 100644 index 00000000..1f411adf --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt @@ -0,0 +1,197 @@ +--TEST-- +Test various error cases with invalid Table-valued parameter inputs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +// Use a different schema instead of dbo +$schema = 'Sales DB'; +$tvpTypeName = 'TestTVP3'; +$procName = 'SelectTVP3'; + +$stmt = sqlsrv_query($conn, "SELECT @@VERSION"); +if (sqlsrv_fetch($stmt)) { + $result = sqlsrv_get_field($stmt, 0); +} +$version = explode(' ', $result); +$pre2016 = ($version[3] < '2016'); + +cleanup($conn, $schema, $tvpTypeName, $procName, $pre2016); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createSchema); +sqlsrv_query($conn, $createTestTVP3); +sqlsrv_query($conn, $createSelectTVP3); + +// Create a TVP input array +$inputs = [ + ['ABC', 12345, null], + ['DEF', 6789, null], + ['GHI', null], +]; +$str = 'dummy'; + +// Case (1) - do not provide TVP type name +$tvpInput = array($inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 1); + +// Case (2) - use an empty string as TVP type name +$tvpInput = array("" => array()); +invokeProc($conn, $callSelectTVP3, $tvpInput, 2); + +// Case (3) - null inputs +$tvpInput = array($tvpTypeName => null); +invokeProc($conn, $callSelectTVP3, $tvpInput, 3); + +// Case (4) - not using array as inputs +$tvpInput = array($tvpTypeName => 1); +invokeProc($conn, $callSelectTVP3, $tvpInput, 4); + +// Case (5) - invalid TVP type name +$tvpInput = array($str => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 5); + +// Case (6) - input rows are not the same size +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 6); + +// Case (7) - input row wrong size +unset($inputs); +$inputs = [ + ['ABC', 12345, null, null] +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 7); + +// Case (8) - use string keys +unset($inputs); +$inputs = [ + ['A' => null, null, null] +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 8); + +// Case (9) - a row is not an array +unset($inputs); +$inputs = [null]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 9); + +// Case (10) - a column value used a string key +unset($inputs); +$inputs = [ + ['ABC', 12345, "key"=>null] +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 10); + +// Case (11) - invalid input object for a TVP column +class foo +{ + function do_foo(){} +} +$bar = new foo; +unset($inputs); +$inputs = [ + ['ABC', 1234, $bar], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 11); + +// Case (12) - invalid input type for a TVP column +unset($inputs); +$inputs = [ + ['ABC', &$str, null], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 12); + +// Case (13) - bind a TVP as an OUTPUT param +invokeProc($conn, $callSelectTVP3, $tvpInput, 13, SQLSRV_PARAM_OUT); + +// Case (14) - test UTF-8 invalid/corrupt string for a TVP column +unset($inputs); +$utf8 = str_repeat("41", 8188); +$utf8 = $utf8 . "e38395e38395"; +$utf8 = substr_replace($utf8, "fe", 1000, 2); +$utf8 = pack("H*", $utf8); + +$inputs = [ + [$utf8, 1234, null], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs, $schema); +invokeProc($conn, $callSelectTVP3, $tvpInput, 14); + +cleanup($conn, $schema, $tvpTypeName, $procName, $pre2016); + +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECTF-- +Error 1: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 2: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 3: Invalid inputs for Table-Valued Param 1 +Error 4: Invalid inputs for Table-Valued Param 1 +Error 5: Failed to get metadata for Table-Valued Param 1 +Error 6: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 7: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 8: Associative arrays not allowed for Table-Valued Param 1 +Error 9: Expect an array for each row for Table-Valued Param 1 +Error 10: Associative arrays not allowed for Table-Valued Param 1 +Error 11: An invalid type for Table-Valued Param 1 Column 3 was specified +Error 12: An invalid type for Table-Valued Param 1 Column 2 was specified +Error 13: You cannot return data in a table-valued parameter. Table-valued parameters are input-only. +Error 14: An error occurred translating a string for Table-Valued Param 1 Column 1 to UTF-16: %a +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt new file mode 100644 index 00000000..97903cee --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt @@ -0,0 +1,149 @@ +--TEST-- +Test Table-valued parameter using prepare/execute and sqlsrv_send_stream_data with one NULL column +--DESCRIPTION-- +Test Table-valued parameter using prepare/execute and sqlsrv_send_stream_data with one column of NULL input values. This test verifies the fetched results of the basic data types. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_000'; + +// 2 - Items TVP +$images = [null, null, null]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$options = array("SendStreamParamsAtExec" => 0); +$stmt = sqlsrv_prepare($conn, $callTVPOrderEntry, $params, $options); +if (!$stmt) { + print_r(sqlsrv_errors()); +} +$res = sqlsrv_execute($stmt); +if (!$res) { + print_r(sqlsrv_errors()); +} + +// Now call sqlsrv_send_stream_data in a loop +while (sqlsrv_send_stream_data($stmt)) { +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch CustID +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); + +if ($result = sqlsrv_fetch( $stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +$stmt = sqlsrv_query($conn, 'SELECT * FROM TVPItem ORDER BY ItemNo'); +while ($row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC)) { + print_r($row); +} + +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 + [Photo] => +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 + [Photo] => +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 + [Photo] => +) +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt new file mode 100644 index 00000000..07b57347 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt @@ -0,0 +1,175 @@ +--TEST-- +Test Table-valued parameter using prepare/execute and some NULL inputs +--DESCRIPTION-- +Test Table-valued parameter using prepare/execute and some NULL inputs. This test fetches results as objects using client buffers. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_789'; + +// 2 - Items TVP +$image3 = fopen($tvpIncPath. $gif3, 'rb'); +$images = [null, null, $image3]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Randomly set some values to null +$items[0][1] = null; +$items[2][3] = null; +$items[0][2] = null; + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$stmt = sqlsrv_prepare($conn, $callTVPOrderEntry, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} +$res = sqlsrv_execute($stmt); +if (!$res) { + print_r(sqlsrv_errors()); +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch the inserted data from the tables +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +if ($result = sqlsrv_fetch( $stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +// Fetch the only image from the table that is not NULL +$tsql = 'SELECT ItemNo, Photo FROM TVPItem WHERE Photo IS NOT NULL ORDER BY ItemNo'; +$stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>"buffered")); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Only the last image is not NULL +$index = 2; +while (sqlsrv_fetch($stmt)) { + $itemNo = sqlsrv_get_field($stmt, 0); + echo $itemNo . PHP_EOL; + $photo = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($images[$index], $photo)) { + echo "Stream data for image $index corrupted!" . PHP_EOL; + } +} +sqlsrv_free_stmt($stmt); +fclose($image3); + +// Fetch the other columns next +$stmt = sqlsrv_query($conn, $selectTVPItemQuery, array(), array("Scrollable"=>"buffered")); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +while ($item = sqlsrv_fetch_object($stmt)) { + print_r($item); +} + +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +3 +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => + [PackedOn] => + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => + [Price] => 88.98 +) +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt new file mode 100644 index 00000000..3a22fe2d --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt @@ -0,0 +1,166 @@ +--TEST-- +Test Table-valued parameter using direct queries and no null values +--DESCRIPTION-- +Test Table-valued parameter using direct queries and no null values. This test verifies the fetched results of all types. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_123'; + +// 2 - Items TVP +$image1 = fopen($tvpIncPath. $gif1, 'rb'); +$image2 = fopen($tvpIncPath. $gif2, 'rb'); +$image3 = fopen($tvpIncPath. $gif3, 'rb'); +$images = [$image1, $image2, $image3]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, null, null, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$stmt = sqlsrv_query($conn, $callTVPOrderEntry, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch CustID +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); + +if ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +// Fetch other basic types +$stmt = sqlsrv_query($conn, $selectTVPItemQuery); +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC)) { + print_r($row); +} +sqlsrv_free_stmt($stmt); + +// Fetch the inserted images from the table and verify them +$tsql = 'SELECT Photo FROM TVPItem ORDER BY ItemNo'; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +$index = 0; +while (sqlsrv_fetch($stmt)) { + $photo = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($images[$index], $photo)) { + echo "Image data corrupted for row ". ($index + 1) . PHP_EOL; + } + $index++; +} +if ($index == 0) { + echo 'Failed in fetching binary data' . PHP_EOL; +} + +sqlsrv_free_stmt($stmt); + +fclose($image1); +fclose($image2); +fclose($image3); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +Array +( + [0] => 1 + [1] => 1 + [2] => 0062836700 + [3] => 367 + [4] => 2009-03-12 + [5] => AWC Tee Male Shirt + [6] => 20.75 +) +Array +( + [0] => 1 + [1] => 2 + [2] => 1250153272 + [3] => 256 + [4] => 2017-11-07 + [5] => Superlight Black Bicycle + [6] => 998.45 +) +Array +( + [0] => 1 + [1] => 3 + [2] => 1328781505 + [3] => 260 + [4] => 2010-03-03 + [5] => Silver Chain for Bikes + [6] => 88.98 +) +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt new file mode 100644 index 00000000..0c63763e --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt @@ -0,0 +1,132 @@ +--TEST-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs +--DESCRIPTION-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +dropProc($conn, 'SelectTVP2'); +$tvpType = 'TestTVP2'; +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createTestTVP2); +sqlsrv_query($conn, $createSelectTVP2); + +// Create column arrays +$str1 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; +$longStr1 = str_repeat($str1, 1500); +$str2 = pack("H*", '49006427500048005000' ); // I'LOVE_SYMBOL'PHP +$longStr2 = str_repeat($str2, 2000); + +$bin1 = pack('H*', '0FD1CEFACE'); +$bin2 = pack('H*', '0001020304'); +$bin3 = hex2bin('616263646566676869'); // abcdefghi +$bin4 = pack('H*', '7A61CC86C7BDCEB2F18FB3BF'); + +$xml = "The quick brown fox jumps over the lazy dog0123456789"; + +$c01 = [null, $str1, $str2]; +$c02 = [null, $longStr1, $longStr2]; +$c03 = [null, null, 999]; +$c04 = [null, 3.1415927, null]; +$c05 = [$bin1, null, $bin2]; +$c06 = [null, $bin3, $bin4]; +$c07 = [null, '1234.56', '9876.54']; +$c08 = [null, null, $xml]; +$c09 = [9876, $str1, (0x0FAB)]; + +// Create a TVP input array +$nrows = 3; +$ncols = 9; +$inputs = array(); +for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($inputs, $rowValues); +} + +$tvpInput = array($tvpType => $inputs); +$params = array(array($tvpInput, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE)); + +$stmt = sqlsrv_query($conn, $callSelectTVP2, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING('UTF-8')); + if ($field != $inputs[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $inputs[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $field . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING('UTF-8')); + var_dump($field); + } + echo PHP_EOL; + $row++; +} +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'SelectTVP2'); +sqlsrv_query($conn, $dropTableType); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Row 1: from Col 3 to 9 +NULL +NULL +string(10) "0FD1CEFACE" +NULL +NULL +NULL +string(4) "9876" + +Row 2: from Col 3 to 9 +NULL +string(9) "3.1415927" +NULL +string(18) "616263646566676869" +string(9) "1234.5600" +NULL +string(46) "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ" + +Row 3: from Col 3 to 9 +string(3) "999" +NULL +string(10) "0001020304" +string(24) "7A61CC86C7BDCEB2F18FB3BF" +string(9) "9876.5400" +string(120) "The quick brown fox jumps over the lazy dog0123456789" +string(4) "4011" + +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt new file mode 100644 index 00000000..d92c4cf5 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt @@ -0,0 +1,131 @@ +--TEST-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs +--DESCRIPTION-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +dropProc($conn, 'SelectTVP'); + +$tvpType = 'TestTVP'; +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createTestTVP); +sqlsrv_query($conn, $createSelectTVP); + +// Create column arrays +$str = ''; +for ($i = 0; $i < 255; $i++) { + $str .= chr(($i % 95) + 32); +} +$longStr = str_repeat($str, 3000); + +$c01 = [$str, 'ABCDE', '']; +$c02 = ['abcdefghijklmnopqrstuvwxyz', null, $longStr]; +$c03 = [null, 0, 1]; +$c04 = [null, + date_create('1997-02-13 12:43:10'), + null]; +$c05 = ["2010-12-31 12:40:12.56679", null, "1965-02-18 23:59:59.43258"]; +$c06 = ['4CDBC69F-F0EE-4963-8F17-24DD47090126', + '0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE', + null]; +$c07 = [null, '-9223372036854775808', '9223372036854775807']; +$c08 = [null, -1.79E+308, 1.79E+308]; +$c09 = ['31234567890123.141243449787580175325274', + '0.000000000000000000000001', + '99999999999999.999999999999999999999999']; + +// Create a TVP input array +$nrows = 3; +$ncols = 9; +$inputs = array(); +for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($inputs, $rowValues); +} + +$tvpInput = array($tvpType => $inputs); +$params = array(array($tvpInput, null, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE)); + +$options = array("SendStreamParamsAtExec" => 0); +$stmt = sqlsrv_query($conn, $callSelectTVP, $params, $options); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Now call sqlsrv_send_stream_data in a loop +while (sqlsrv_send_stream_data($stmt)) { +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + if ($field != $inputs[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $inputs[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $field . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + $field = sqlsrv_get_field($stmt, $col); + var_dump($field); + } + echo PHP_EOL; + $row++; +} +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'SelectTVP'); +sqlsrv_query($conn, $dropTableType); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Row 1: from Col 3 to 9 +NULL +NULL +string(25) "2010-12-31 12:40:12.56679" +string(36) "4CDBC69F-F0EE-4963-8F17-24DD47090126" +NULL +NULL +string(39) "31234567890123.141243449787580175325274" + +Row 2: from Col 3 to 9 +int(0) +string(19) "1997-02-13 12:43:00" +NULL +string(36) "0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE" +string(20) "-9223372036854775808" +float(-1.79E+308) +string(25) ".000000000000000000000001" + +Row 3: from Col 3 to 9 +int(1) +NULL +string(25) "1965-02-18 23:59:59.43258" +NULL +string(19) "9223372036854775807" +float(1.79E+308) +string(39) "99999999999999.999999999999999999999999" + +Done diff --git a/test/functional/sqlsrv/srv_1063_test_locale.php b/test/functional/sqlsrv/srv_1063_test_locale.php index 2c8eb56d..b8fea4d5 100644 --- a/test/functional/sqlsrv/srv_1063_test_locale.php +++ b/test/functional/sqlsrv/srv_1063_test_locale.php @@ -30,6 +30,22 @@ function printMoney($amt, $info) } } +function printCal($date) +{ + $loc = setlocale(LC_TIME, 0); + $fmt = datefmt_create( + $loc, + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + 'America/Los_Angeles', + IntlDateFormatter::GREGORIAN + ); + datefmt_set_pattern($fmt, 'cccc'); + echo datefmt_format($fmt, $date) . PHP_EOL; + datefmt_set_pattern($fmt, 'MMMM'); + echo datefmt_format($fmt, $date) . PHP_EOL; +} + require_once('MsSetup.inc'); $setLocaleInfo = ($_SERVER['argv'][1]); @@ -96,8 +112,8 @@ if ($sep !== $info['thousands_sep']) { $n1 = 10000.98765; printMoney($n1, $info); -echo strftime("%A", strtotime("12/25/2020")) . PHP_EOL; -echo strftime("%B", strtotime("12/25/2020")) . PHP_EOL; +$d = new DateTime("12/25/2020", new DateTimeZone('America/Los_Angeles')); +printCal($d); $conn = sqlsrv_connect($server, $connectionOptions); if (!$conn) { diff --git a/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt b/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt new file mode 100644 index 00000000..7aba7464 --- /dev/null +++ b/test/functional/sqlsrv/srv_1261_test_ascii_utf8.phpt @@ -0,0 +1,39 @@ +--TEST-- +Verify Github Issue 1261 is fixed. +--DESCRIPTION-- +This test should already pass in Windows so it is mainly aimed for non-Windows settings where UTF-8 is the default encoding. +--SKIPIF-- + +--FILE-- + SQLSRV_ENC_CHAR)); + +# leading string >= 2045 leading to result length > 2048 +# column must be VARCHAR(MAX) or VARCHAR(2048) (starts working with bigger VARCHAR(n), e.g. 2060) +# 'CharacterSet' connInfo must be set to SQLSRV_ENC_CHAR (works with UTF-8) +# COLLATE must not be %UTF8% (e.g. Latin1_General_100_CI_AS_SC_UTF8 works) + +$sql = "SET NOCOUNT ON; + DECLARE @val VARCHAR(8000) = REPLICATE('a', 2045) + 'ñ'; + CREATE TABLE #tmpTest (testCol VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CI_AS); + INSERT INTO #tmpTest (testCol) VALUES (@val); + SELECT * from #tmpTest;"; + +$stmt = sqlsrv_query($conn, $sql); +$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); +$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); +var_dump($row, $errors); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); +?> +--EXPECT-- +array(1) { + ["testCol"]=> + string(2049) "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaañ" +} +NULL \ No newline at end of file diff --git a/test/functional/sqlsrv/tools.inc b/test/functional/sqlsrv/tools.inc index ad2899c9..275dfdec 100644 --- a/test/functional/sqlsrv/tools.inc +++ b/test/functional/sqlsrv/tools.inc @@ -218,7 +218,7 @@ function compareDataStream($colType, $rowIndex, $colName, $stream, $expected) function compareBinaryData($actual, $expected) { // this function assumes $actual is a stream of hex - $len = strlen($expected); + $len = (empty($expected)) ? 0 : strlen($expected); $pos = 0; $matched = true;